diff options
| author | David Göransson <david.goransson@mullvad.net> | 2024-10-18 09:39:17 +0200 |
|---|---|---|
| committer | David Göransson <david.goransson@mullvad.net> | 2024-10-18 09:39:17 +0200 |
| commit | 8ffc8d3a6f2f2598566791a2bf87d05b316b1b18 (patch) | |
| tree | f5af089ca00b821b8523502d88f19183db017e16 /android | |
| parent | a6b2db5e8ffefc4197edf7b092bf47662f365fb9 (diff) | |
| parent | b71e78a641788a0660cc4da5a4ae40b2b18014b4 (diff) | |
| download | mullvadvpn-8ffc8d3a6f2f2598566791a2bf87d05b316b1b18.tar.xz mullvadvpn-8ffc8d3a6f2f2598566791a2bf87d05b316b1b18.zip | |
Merge branch 'move-changelog-to-settings-droid-1414'
Diffstat (limited to 'android')
29 files changed, 356 insertions, 271 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogDialogTest.kt index 48161a4690..b8165f80fc 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogDialogTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogDialogTest.kt @@ -4,16 +4,12 @@ import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.just -import io.mockk.verify import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension import net.mullvad.mullvadvpn.compose.dialog.ChangelogDialog import net.mullvad.mullvadvpn.compose.setContentWithTheme -import net.mullvad.mullvadvpn.viewmodel.Changelog -import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel +import net.mullvad.mullvadvpn.viewmodel.AppInfoViewModel +import net.mullvad.mullvadvpn.viewmodel.ChangelogUiState import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @@ -22,7 +18,7 @@ import org.junit.jupiter.api.extension.RegisterExtension class ChangelogDialogTest { @JvmField @RegisterExtension val composeExtension = createEdgeToEdgeComposeExtension() - @MockK lateinit var mockedViewModel: ChangelogViewModel + @MockK lateinit var mockedViewModel: AppInfoViewModel @BeforeEach fun setup() { @@ -33,13 +29,10 @@ class ChangelogDialogTest { fun testShowChangeLogWhenNeeded() = composeExtension.use { // Arrange - // Arrange - every { mockedViewModel.markChangelogAsRead() } just Runs - setContentWithTheme { ChangelogDialog( - Changelog(changes = listOf(CHANGELOG_ITEM), version = CHANGELOG_VERSION), - onDismiss = { mockedViewModel.markChangelogAsRead() }, + ChangelogUiState(changes = listOf(CHANGELOG_ITEM), version = CHANGELOG_VERSION), + onDismiss = {}, ) } @@ -48,9 +41,6 @@ class ChangelogDialogTest { // perform click on Got It button to check if dismiss occur onNodeWithText(CHANGELOG_BUTTON_TEXT).performClick() - - // Assert - verify { mockedViewModel.markChangelogAsRead() } } companion object { 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 f55e07c1cf..05862a5bc4 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 @@ -594,7 +594,7 @@ class ConnectScreenTest { val versionInfo = VersionInfo(isSupported = false, currentVersion = "") setContentWithTheme { ConnectScreen( - onUpdateVersionClick = mockedClickHandler, + onOpenAppListing = mockedClickHandler, state = ConnectUiState( location = null, diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreenTest.kt index f108efab8e..2509c7be8d 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreenTest.kt @@ -39,7 +39,7 @@ class SettingsScreenTest { // Assert onNodeWithText("VPN settings").assertExists() onNodeWithText("Split tunneling").assertExists() - onNodeWithText("App version").assertExists() + onNodeWithText("App info").assertExists() onNodeWithText("API access").assertExists() } @@ -62,7 +62,7 @@ class SettingsScreenTest { // Assert onNodeWithText("VPN settings").assertDoesNotExist() onNodeWithText("Split tunneling").assertDoesNotExist() - onNodeWithText("App version").assertExists() + onNodeWithText("App info").assertExists() onNodeWithText("API access").assertExists() } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/NavigationComposeCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/NavigationComposeCell.kt index 0fd7674c89..ece54c9102 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/NavigationComposeCell.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/NavigationComposeCell.kt @@ -136,7 +136,7 @@ internal fun NavigationCellBody( verticalAlignment = Alignment.CenterVertically, modifier = modifier.wrapContentWidth().wrapContentHeight(), ) { - Text(text = content, style = MaterialTheme.typography.labelMedium, color = textColor) + Text(text = content, style = MaterialTheme.typography.bodyMedium, color = textColor) Spacer(modifier = Modifier.width(Dimens.sideMargin)) if (isExternalLink) { DefaultExternalLinkView(content, tint = contentColor) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/TwoRowCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/TwoRowCell.kt index 4c86a33452..b0f2d63ace 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/TwoRowCell.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/TwoRowCell.kt @@ -32,8 +32,8 @@ fun TwoRowCell( iconView: @Composable RowScope.() -> Unit = {}, onCellClicked: (() -> Unit)? = null, titleColor: Color = MaterialTheme.colorScheme.onPrimary, - subtitleColor: Color = MaterialTheme.colorScheme.onPrimary, - titleStyle: TextStyle = MaterialTheme.typography.labelLarge, + subtitleColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + titleStyle: TextStyle = MaterialTheme.typography.titleMedium, subtitleStyle: TextStyle = MaterialTheme.typography.labelLarge, background: Color = MaterialTheme.colorScheme.primary, endPadding: Dp = Dimens.cellEndPadding, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt index df7d3dede0..f2b33c251e 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt @@ -77,7 +77,7 @@ fun NotificationBanner( modifier: Modifier = Modifier, notification: InAppNotification?, isPlayBuild: Boolean, - onClickUpdateVersion: () -> Unit, + openAppListing: () -> Unit, onClickShowAccount: () -> Unit, onClickDismissNewDevice: () -> Unit, ) { @@ -94,7 +94,7 @@ fun NotificationBanner( Notification( visibleNotification.toNotificationData( isPlayBuild = isPlayBuild, - onClickUpdateVersion, + openAppListing, onClickShowAccount, onClickDismissNewDevice, ) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt index de36f76ac7..5318933852 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt @@ -44,7 +44,7 @@ data class NotificationAction( @Composable fun InAppNotification.toNotificationData( isPlayBuild: Boolean, - onClickUpdateVersion: () -> Unit, + openAppListing: () -> Unit, onClickShowAccount: () -> Unit, onDismissNewDevice: () -> Unit, ) = @@ -101,13 +101,11 @@ fun InAppNotification.toNotificationData( message = stringResource(id = R.string.unsupported_version_description), statusLevel = StatusLevel.Error, action = - if (isPlayBuild) null - else - NotificationAction( - Icons.AutoMirrored.Default.OpenInNew, - onClickUpdateVersion, - stringResource(id = R.string.open_url), - ), + NotificationAction( + Icons.AutoMirrored.Default.OpenInNew, + openAppListing, + stringResource(id = R.string.open_url), + ), ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ChangelogDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ChangelogDialog.kt index d2e96dc1d9..67a6aac2fe 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ChangelogDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ChangelogDialog.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph @@ -24,26 +25,21 @@ import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.button.PrimaryButton import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens -import net.mullvad.mullvadvpn.viewmodel.Changelog +import net.mullvad.mullvadvpn.viewmodel.ChangelogUiState import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel import org.koin.androidx.compose.koinViewModel @Destination<RootGraph>(style = DestinationStyle.Dialog::class) @Composable -fun Changelog(navController: NavController, changeLog: Changelog) { +fun Changelog(navController: NavController) { val viewModel = koinViewModel<ChangelogViewModel>() + val uiState = viewModel.uiState.collectAsStateWithLifecycle() - ChangelogDialog( - changeLog, - onDismiss = { - viewModel.markChangelogAsRead() - navController.navigateUp() - }, - ) + ChangelogDialog(uiState.value, onDismiss = { navController.navigateUp() }) } @Composable -fun ChangelogDialog(changeLog: Changelog, onDismiss: () -> Unit) { +fun ChangelogDialog(changeLog: ChangelogUiState, onDismiss: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, title = { @@ -103,7 +99,10 @@ private fun ChangeListItem(text: String) { @Composable private fun PreviewChangelogDialogWithSingleShortItem() { AppTheme { - ChangelogDialog(Changelog(changes = listOf("Item 1"), version = "1111.1"), onDismiss = {}) + ChangelogDialog( + ChangelogUiState(changes = listOf("Item 1"), version = "1111.1"), + onDismiss = {}, + ) } } @@ -117,7 +116,10 @@ private fun PreviewChangelogDialogWithTwoLongItems() { AppTheme { ChangelogDialog( - Changelog(changes = listOf(longPreviewText, longPreviewText), version = "1111.1"), + ChangelogUiState( + changes = listOf(longPreviewText, longPreviewText), + version = "1111.1", + ), onDismiss = {}, ) } @@ -128,7 +130,7 @@ private fun PreviewChangelogDialogWithTwoLongItems() { private fun PreviewChangelogDialogWithTenShortItems() { AppTheme { ChangelogDialog( - Changelog( + ChangelogUiState( changes = listOf( "Item 1", diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ApiAccessListScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ApiAccessListScreen.kt index 5dc2fa6d30..c1fc183cfa 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ApiAccessListScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ApiAccessListScreen.kt @@ -186,8 +186,6 @@ private fun ApiAccessMethodItem( R.string.off } ), - titleStyle = MaterialTheme.typography.titleMedium, - subtitleColor = MaterialTheme.colorScheme.onSurfaceVariant, bodyView = { Icon( Icons.Default.ChevronRight, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AppInfoScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AppInfoScreen.kt new file mode 100644 index 0000000000..e65dc2c8d8 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AppInfoScreen.kt @@ -0,0 +1,155 @@ +package net.mullvad.mullvadvpn.compose.screen + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.OpenInNew +import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.compose.dropUnlessResumed +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.generated.destinations.ChangelogDestination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.cell.NavigationComposeCell +import net.mullvad.mullvadvpn.compose.cell.TwoRowCell +import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton +import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition +import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle +import net.mullvad.mullvadvpn.lib.theme.Dimens +import net.mullvad.mullvadvpn.viewmodel.AppInfoSideEffect +import net.mullvad.mullvadvpn.viewmodel.AppInfoUiState +import net.mullvad.mullvadvpn.viewmodel.AppInfoViewModel +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Destination<RootGraph>(style = SlideInFromRightTransition::class) +@Composable +fun AppInfo(navigator: DestinationsNavigator) { + val vm = koinViewModel<AppInfoViewModel>() + val state by vm.uiState.collectAsStateWithLifecycle() + + val uriHandler = LocalUriHandler.current + + CollectSideEffectWithLifecycle(vm.uiSideEffect) { + when (it) { + is AppInfoSideEffect.OpenUri -> uriHandler.openUri(it.uri.toString()) + } + } + + AppInfo( + state = state, + onBackClick = dropUnlessResumed { navigator.navigateUp() }, + navigateToChangelog = dropUnlessResumed { navigator.navigate(ChangelogDestination) }, + openAppListing = dropUnlessResumed { vm.openAppListing() }, + ) +} + +@ExperimentalMaterial3Api +@Composable +fun AppInfo( + state: AppInfoUiState, + onBackClick: () -> Unit, + navigateToChangelog: () -> Unit, + openAppListing: () -> Unit, +) { + ScaffoldWithMediumTopBar( + appBarTitle = stringResource(id = R.string.app_info), + navigationIcon = { NavigateBackIconButton(onNavigateBack = onBackClick) }, + ) { modifier -> + Column(horizontalAlignment = Alignment.Start, modifier = modifier.animateContentSize()) { + AppInfoContent(state, navigateToChangelog, openAppListing) + } + } +} + +@Composable +fun AppInfoContent( + state: AppInfoUiState, + navigateToChangelog: () -> Unit, + openAppListing: () -> Unit, +) { + Column(modifier = Modifier.padding(bottom = Dimens.smallPadding).animateContentSize()) { + AppVersionRow(state, openAppListing) + + ChangelogRow(navigateToChangelog) + } +} + +@Composable +private fun AppVersionRow(state: AppInfoUiState, openAppListing: () -> Unit) { + Column { + TwoRowCell( + titleText = stringResource(id = R.string.version), + subtitleText = state.version.currentVersion, + iconView = { + if (!state.version.isSupported) { + Icon( + imageVector = Icons.Default.Error, + modifier = Modifier.padding(end = Dimens.smallPadding), + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + } + }, + bodyView = { + Icon( + Icons.AutoMirrored.Default.OpenInNew, + contentDescription = stringResource(R.string.app_info), + tint = MaterialTheme.colorScheme.onPrimary, + ) + }, + onCellClicked = openAppListing, + ) + + if (!state.version.isSupported) { + Text( + text = stringResource(id = R.string.unsupported_version_description), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = + Modifier.fillMaxWidth() + .padding( + start = Dimens.cellStartPadding, + end = Dimens.cellStartPadding, + top = Dimens.smallPadding, + bottom = Dimens.mediumPadding, + ), + ) + } else { + HorizontalDivider(color = Color.Transparent) + } + } +} + +@Composable +private fun ChangelogRow(navigateToChangelog: () -> Unit) { + NavigationComposeCell( + title = stringResource(R.string.changelog_title), + onClick = navigateToChangelog, + bodyView = { + Icon( + imageVector = Icons.Default.Info, + contentDescription = stringResource(R.string.changelog_title), + tint = MaterialTheme.colorScheme.onPrimary, + ) + }, + ) +} 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 a9d47893b1..9e99ae2daa 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 @@ -1,8 +1,6 @@ package net.mullvad.mullvadvpn.compose.screen import android.content.Context -import android.content.Intent -import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.animation.AnimatedContent import androidx.compose.animation.animateColorAsState @@ -51,6 +49,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed +import co.touchlab.kermit.Logger import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.NavGraphs @@ -109,7 +108,6 @@ import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible import net.mullvad.mullvadvpn.lib.theme.typeface.connectionStatus import net.mullvad.mullvadvpn.lib.theme.typeface.hostname -import net.mullvad.mullvadvpn.util.appendHideNavOnPlayBuild import net.mullvad.mullvadvpn.util.removeHtmlTags import net.mullvad.mullvadvpn.viewmodel.ConnectViewModel import org.koin.androidx.compose.koinViewModel @@ -147,6 +145,7 @@ fun Connect( } val openAccountPage = LocalUriHandler.current.createOpenAccountPageHook() + val uriHandler = LocalUriHandler.current CollectSideEffectWithLifecycle( connectViewModel.uiSideEffect, minActiveState = Lifecycle.State.RESUMED, @@ -175,6 +174,14 @@ fun Connect( message = sideEffect.toMessage(context) ) } + + is ConnectViewModel.UiSideEffect.OpenUri -> { + try { + uriHandler.openUri(sideEffect.uri.toString()) + } catch (e: IllegalArgumentException) { + Logger.w("Failed to open uri", e) + } + } } } @@ -192,19 +199,7 @@ fun Connect( onConnectClick = connectViewModel::onConnectClick, onCancelClick = connectViewModel::onCancelClick, onSwitchLocationClick = dropUnlessResumed { navigator.navigate(SelectLocationDestination) }, - onUpdateVersionClick = { - val intent = - Intent( - Intent.ACTION_VIEW, - Uri.parse( - context - .getString(R.string.download_url) - .appendHideNavOnPlayBuild(state.isPlayBuild) - ), - ) - .apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK } - context.startActivity(intent) - }, + onOpenAppListing = connectViewModel::openAppListing, onManageAccountClick = connectViewModel::onManageAccountClick, onSettingsClick = dropUnlessResumed { navigator.navigate(SettingsDestination) }, onAccountClick = dropUnlessResumed { navigator.navigate(AccountDestination) }, @@ -221,7 +216,7 @@ fun ConnectScreen( onConnectClick: () -> Unit = {}, onCancelClick: () -> Unit = {}, onSwitchLocationClick: () -> Unit = {}, - onUpdateVersionClick: () -> Unit = {}, + onOpenAppListing: () -> Unit = {}, onManageAccountClick: () -> Unit = {}, onSettingsClick: () -> Unit = {}, onAccountClick: () -> Unit = {}, @@ -268,7 +263,7 @@ fun ConnectScreen( NotificationBanner( notification = state.inAppNotification, isPlayBuild = state.isPlayBuild, - onClickUpdateVersion = onUpdateVersionClick, + openAppListing = onOpenAppListing, onClickShowAccount = onManageAccountClick, onClickDismissNewDevice = onDismissNewDeviceClick, ) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt index 96c9a63ce2..332992c4e5 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt @@ -13,26 +13,17 @@ import androidx.navigation.NavHostController import co.touchlab.kermit.Logger import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.generated.NavGraphs -import com.ramcosta.composedestinations.generated.destinations.ChangelogDestination -import com.ramcosta.composedestinations.generated.destinations.ConnectDestination import com.ramcosta.composedestinations.generated.destinations.NoDaemonDestination -import com.ramcosta.composedestinations.generated.destinations.OutOfTimeDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.rememberNavHostEngine -import com.ramcosta.composedestinations.utils.destination import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map import net.mullvad.mullvadvpn.compose.util.RequestVpnPermission -import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel import net.mullvad.mullvadvpn.viewmodel.DaemonScreenEvent import net.mullvad.mullvadvpn.viewmodel.NoDaemonViewModel import net.mullvad.mullvadvpn.viewmodel.VpnPermissionSideEffect import net.mullvad.mullvadvpn.viewmodel.VpnPermissionViewModel import org.koin.androidx.compose.koinViewModel -private val changeLogDestinations = listOf(ConnectDestination, OutOfTimeDestination) - @OptIn(ExperimentalComposeUiApi::class) @Composable fun MullvadApp() { @@ -71,19 +62,6 @@ fun MullvadApp() { } } - // Globally show the changelog - val changeLogsViewModel = koinViewModel<ChangelogViewModel>() - LaunchedEffect(Unit) { - changeLogsViewModel.uiSideEffect.collect { - // Wait until we are in an acceptable destination - navHostController.currentBackStackEntryFlow - .map { it.destination() } - .first { it in changeLogDestinations } - - navigator.navigate(ChangelogDestination(it)) - } - } - // Ask for VPN Permission val launchVpnPermission = rememberLauncherForActivityResult(RequestVpnPermission()) { _ -> permissionVm.connect() } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt index 8a50649ac8..eaa8cc7933 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt @@ -3,14 +3,17 @@ package net.mullvad.mullvadvpn.compose.screen import android.content.Context import android.net.Uri import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.Row 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.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material.icons.filled.Error import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -24,14 +27,15 @@ import androidx.lifecycle.compose.dropUnlessResumed import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.ApiAccessListDestination +import com.ramcosta.composedestinations.generated.destinations.AppInfoDestination import com.ramcosta.composedestinations.generated.destinations.ReportProblemDestination import com.ramcosta.composedestinations.generated.destinations.SplitTunnelingDestination import com.ramcosta.composedestinations.generated.destinations.VpnSettingsDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.cell.DefaultExternalLinkView -import net.mullvad.mullvadvpn.compose.cell.NavigationCellBody import net.mullvad.mullvadvpn.compose.cell.NavigationComposeCell +import net.mullvad.mullvadvpn.compose.cell.TwoRowCell import net.mullvad.mullvadvpn.compose.component.NavigateCloseIconButton import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar import net.mullvad.mullvadvpn.compose.extensions.itemWithDivider @@ -66,6 +70,7 @@ fun Settings(navigator: DestinationsNavigator) { onVpnSettingCellClick = dropUnlessResumed { navigator.navigate(VpnSettingsDestination) }, onSplitTunnelingCellClick = dropUnlessResumed { navigator.navigate(SplitTunnelingDestination) }, + onAppInfoClick = dropUnlessResumed { navigator.navigate(AppInfoDestination) }, onApiAccessClick = dropUnlessResumed { navigator.navigate(ApiAccessListDestination) }, onReportProblemCellClick = dropUnlessResumed { navigator.navigate(ReportProblemDestination) }, @@ -79,6 +84,7 @@ fun SettingsScreen( state: SettingsUiState, onVpnSettingCellClick: () -> Unit = {}, onSplitTunnelingCellClick: () -> Unit = {}, + onAppInfoClick: () -> Unit = {}, onReportProblemCellClick: () -> Unit = {}, onApiAccessClick: () -> Unit = {}, onBackClick: () -> Unit = {}, @@ -114,7 +120,7 @@ fun SettingsScreen( } item { Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing)) } - item { AppVersion(context, state) } + item { AppInfo(onAppInfoClick, state) } item { Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing)) } @@ -138,54 +144,29 @@ private fun SplitTunneling(onSplitTunnelingCellClick: () -> Unit) { } @Composable -private fun AppVersion(context: Context, state: SettingsUiState) { - NavigationComposeCell( - title = stringResource(id = R.string.app_version), - onClick = { - context.openLink( - Uri.parse( - context.resources - .getString(R.string.download_url) - .appendHideNavOnPlayBuild(state.isPlayBuild) - ) - ) - }, - bodyView = - @Composable { - if (!state.isPlayBuild) { - NavigationCellBody( - content = state.appVersion, - contentBodyDescription = stringResource(id = R.string.app_version), - textColor = MaterialTheme.colorScheme.onSurfaceVariant, - isExternalLink = true, - ) - } else { - Text( - text = state.appVersion, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, +private fun AppInfo(navigateToAppInfo: () -> Unit, state: SettingsUiState) { + TwoRowCell( + titleText = stringResource(id = R.string.app_info), + subtitleText = state.appVersion, + bodyView = { + Row { + if (!state.isSupportedVersion) { + Icon( + imageVector = Icons.Default.Error, + modifier = Modifier.padding(end = Dimens.smallPadding), + contentDescription = null, + tint = MaterialTheme.colorScheme.error, ) } - }, - showWarning = !state.isSupportedVersion, - isRowEnabled = !state.isPlayBuild, + Icon( + Icons.Default.ChevronRight, + contentDescription = stringResource(R.string.app_info), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } + }, + onCellClicked = navigateToAppInfo, ) - - if (!state.isSupportedVersion) { - Text( - text = stringResource(id = R.string.unsupported_version_description), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = - Modifier.fillMaxWidth() - .padding( - start = Dimens.cellStartPadding, - top = Dimens.cellTopPadding, - end = Dimens.cellStartPadding, - bottom = Dimens.cellLabelVerticalPadding, - ), - ) - } } @Composable 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 e00b3d9b98..2605075ef8 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 @@ -54,6 +54,7 @@ import net.mullvad.mullvadvpn.util.IChangelogDataProvider import net.mullvad.mullvadvpn.viewmodel.AccountViewModel import net.mullvad.mullvadvpn.viewmodel.ApiAccessListViewModel import net.mullvad.mullvadvpn.viewmodel.ApiAccessMethodDetailsViewModel +import net.mullvad.mullvadvpn.viewmodel.AppInfoViewModel import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel import net.mullvad.mullvadvpn.viewmodel.ConnectViewModel import net.mullvad.mullvadvpn.viewmodel.CreateCustomListDialogViewModel @@ -118,7 +119,7 @@ val uiModule = module { single { androidContext().assets } single { androidContext().contentResolver } - single { ChangelogRepository(get(named(APP_PREFERENCES_NAME)), get()) } + single { ChangelogRepository(get()) } single { PrivacyDisclaimerRepository( androidContext().getSharedPreferences(APP_PREFERENCES_NAME, Context.MODE_PRIVATE) @@ -181,7 +182,10 @@ val uiModule = module { // View models viewModel { AccountViewModel(get(), get(), get(), IS_PLAY_BUILD) } - viewModel { ChangelogViewModel(get(), get(), BuildConfig.ALWAYS_SHOW_CHANGELOG) } + viewModel { ChangelogViewModel(get(), get()) } + viewModel { + AppInfoViewModel(get(), get(), get(), IS_PLAY_BUILD, get(named(SELF_PACKAGE_NAME))) + } viewModel { ConnectViewModel( get(), @@ -194,7 +198,9 @@ val uiModule = module { get(), get(), get(), + get(), IS_PLAY_BUILD, + get(named(SELF_PACKAGE_NAME)), ) } viewModel { DeviceListViewModel(get(), get()) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepository.kt index fcc07693c8..5267f52271 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepository.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepository.kt @@ -1,24 +1,12 @@ package net.mullvad.mullvadvpn.repository -import android.content.SharedPreferences import net.mullvad.mullvadvpn.util.IChangelogDataProvider import net.mullvad.mullvadvpn.util.trimAll -private const val MISSING_VERSION_CODE = -1 private const val NEWLINE_CHAR = '\n' private const val BULLET_POINT_CHAR = '-' -private const val LAST_SHOWED_CHANGELOG_VERSION_CODE = "last_showed_changelog_version_code" -class ChangelogRepository( - private val preferences: SharedPreferences, - private val dataProvider: IChangelogDataProvider, -) { - fun getVersionCodeOfMostRecentChangelogShowed(): Int { - return preferences.getInt(LAST_SHOWED_CHANGELOG_VERSION_CODE, MISSING_VERSION_CODE) - } - - fun setVersionCodeOfMostRecentChangelogShowed(versionCode: Int) = - preferences.edit().putInt(LAST_SHOWED_CHANGELOG_VERSION_CODE, versionCode).apply() +class ChangelogRepository(private val dataProvider: IChangelogDataProvider) { fun getLastVersionChanges(): List<String> = // Prepend with a new line char so each entry consists of NEWLINE_CHAR + BULLET_POINT_CHAR diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AppVersionInfoRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AppVersionInfoRepository.kt index 6f8b92f304..7a74c0f0d2 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AppVersionInfoRepository.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AppVersionInfoRepository.kt @@ -13,10 +13,10 @@ import net.mullvad.mullvadvpn.ui.VersionInfo class AppVersionInfoRepository( private val buildVersion: BuildVersion, - private val managementService: ManagementService, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO, + managementService: ManagementService, + dispatcher: CoroutineDispatcher = Dispatchers.IO, ) { - fun versionInfo(): StateFlow<VersionInfo> = + val versionInfo: StateFlow<VersionInfo> = managementService.versionInfo .map { appVersionInfo -> VersionInfo( diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryInAppNotificationUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryInAppNotificationUseCase.kt index 004dd44351..9b71297d36 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryInAppNotificationUseCase.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryInAppNotificationUseCase.kt @@ -1,9 +1,11 @@ package net.mullvad.mullvadvpn.usecase import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import net.mullvad.mullvadvpn.lib.shared.AccountRepository import net.mullvad.mullvadvpn.repository.InAppNotification import net.mullvad.mullvadvpn.service.notifications.accountexpiry.ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD @@ -24,8 +26,10 @@ class AccountExpiryInAppNotificationUseCase(private val accountRepository: Accou ) .map { expiresInPeriod -> InAppNotification.AccountExpiry(expiresInPeriod) } } else { - flowOf<InAppNotification?>(null) + flowOf(null) } } .map(::listOfNotNull) + .onStart { emit(emptyList()) } + .distinctUntilChanged() } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCase.kt index 18d4e2fc3e..d46089a9d3 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCase.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCase.kt @@ -12,8 +12,7 @@ class VersionNotificationUseCase( ) { operator fun invoke() = - appVersionInfoRepository - .versionInfo() + appVersionInfoRepository.versionInfo .map { versionInfo -> listOfNotNull(unsupportedVersionNotification(versionInfo)) } .distinctUntilChanged() diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AppInfoViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AppInfoViewModel.kt new file mode 100644 index 0000000000..662cbdc4a1 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AppInfoViewModel.kt @@ -0,0 +1,69 @@ +package net.mullvad.mullvadvpn.viewmodel + +import android.content.res.Resources +import android.net.Uri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.repository.ChangelogRepository +import net.mullvad.mullvadvpn.ui.VersionInfo +import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository + +class AppInfoViewModel( + changelogRepository: ChangelogRepository, + appVersionInfoRepository: AppVersionInfoRepository, + private val resources: Resources, + private val isPlayBuild: Boolean, + private val packageName: String, +) : ViewModel() { + + private val _uiSideEffect = Channel<AppInfoSideEffect>() + val uiSideEffect = _uiSideEffect.receiveAsFlow() + + val uiState: StateFlow<AppInfoUiState> = + combine( + appVersionInfoRepository.versionInfo, + flowOf(changelogRepository.getLastVersionChanges()), + flowOf(isPlayBuild), + ) { versionInfo, changes, isPlayBuild -> + AppInfoUiState(versionInfo, changes, isPlayBuild) + } + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(), + AppInfoUiState( + appVersionInfoRepository.versionInfo.value, + changelogRepository.getLastVersionChanges(), + true, + ), + ) + + fun openAppListing() = + viewModelScope.launch { + val uri = + if (isPlayBuild) { + resources.getString(R.string.market_uri, packageName) + } else { + resources.getString(R.string.download_url) + } + _uiSideEffect.send(AppInfoSideEffect.OpenUri(Uri.parse(uri))) + } +} + +data class AppInfoUiState( + val version: VersionInfo, + val changes: List<String>, + val isPlayBuild: Boolean, +) + +sealed interface AppInfoSideEffect { + data class OpenUri(val uri: Uri) : AppInfoSideEffect +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt index f0817ea4fe..571d5da3e3 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt @@ -2,39 +2,18 @@ package net.mullvad.mullvadvpn.viewmodel import android.os.Parcelable import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.parcelize.Parcelize import net.mullvad.mullvadvpn.lib.model.BuildVersion import net.mullvad.mullvadvpn.repository.ChangelogRepository -class ChangelogViewModel( - private val changelogRepository: ChangelogRepository, - private val buildVersion: BuildVersion, - private val alwaysShowChangelog: Boolean, -) : ViewModel() { - - private val _uiSideEffect = MutableSharedFlow<Changelog>(replay = 1, extraBufferCapacity = 1) - val uiSideEffect: SharedFlow<Changelog> = _uiSideEffect - - init { - if (shouldShowChangelog()) { - val changelog = - Changelog(buildVersion.name, changelogRepository.getLastVersionChanges()) - viewModelScope.launch { _uiSideEffect.emit(changelog) } - } - } - - fun markChangelogAsRead() { - changelogRepository.setVersionCodeOfMostRecentChangelogShowed(buildVersion.code) - } - - private fun shouldShowChangelog(): Boolean = - alwaysShowChangelog || - (changelogRepository.getVersionCodeOfMostRecentChangelogShowed() < buildVersion.code && - changelogRepository.getLastVersionChanges().isNotEmpty()) +class ChangelogViewModel(changelogRepository: ChangelogRepository, buildVersion: BuildVersion) : + ViewModel() { + val uiState: StateFlow<ChangelogUiState> = + MutableStateFlow( + ChangelogUiState(buildVersion.name, changelogRepository.getLastVersionChanges()) + ) } -@Parcelize data class Changelog(val version: String, val changes: List<String>) : Parcelable +@Parcelize data class ChangelogUiState(val version: String, val changes: List<String>) : Parcelable 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 33d81f3ba1..d9ca922f1f 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 @@ -1,5 +1,7 @@ package net.mullvad.mullvadvpn.viewmodel +import android.content.res.Resources +import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.FlowPreview @@ -14,6 +16,7 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.state.ConnectUiState import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect import net.mullvad.mullvadvpn.lib.model.ConnectError @@ -46,7 +49,9 @@ class ConnectViewModel( private val connectionProxy: ConnectionProxy, lastKnownLocationUseCase: LastKnownLocationUseCase, private val vpnPermissionRepository: VpnPermissionRepository, + private val resources: Resources, private val isPlayBuild: Boolean, + private val packageName: String, ) : ViewModel() { private val _uiSideEffect = Channel<UiSideEffect>() @@ -169,6 +174,17 @@ class ConnectViewModel( } } + fun openAppListing() = + viewModelScope.launch { + val uri = + if (isPlayBuild) { + resources.getString(R.string.market_uri, packageName) + } else { + resources.getString(R.string.download_url) + } + _uiSideEffect.send(UiSideEffect.OpenUri(Uri.parse(uri))) + } + fun dismissNewDeviceNotification() { newDeviceRepository.clearNewDeviceCreatedNotification() } @@ -186,6 +202,8 @@ class ConnectViewModel( data object OutOfTime : UiSideEffect + data class OpenUri(val uri: Uri) : UiSideEffect + data object RevokedDevice : UiSideEffect data object NoVpnPermission : UiSideEffect diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt index 4cd11f2cc4..fc6b4af3ee 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt @@ -18,7 +18,7 @@ class SettingsViewModel( ) : ViewModel() { val uiState: StateFlow<SettingsUiState> = - combine(deviceRepository.deviceState, appVersionInfoRepository.versionInfo()) { + combine(deviceRepository.deviceState, appVersionInfoRepository.versionInfo) { deviceState, versionInfo -> SettingsUiState( diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepositoryTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepositoryTest.kt index e38d6bebbc..1524549e57 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepositoryTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/ChangelogRepositoryTest.kt @@ -1,6 +1,5 @@ package net.mullvad.mullvadvpn.repository -import android.content.SharedPreferences import io.mockk.every import io.mockk.mockk import net.mullvad.mullvadvpn.lib.common.test.assertLists @@ -9,11 +8,9 @@ import org.junit.jupiter.api.Test class ChangelogRepositoryTest { - private val mockedPreferences: SharedPreferences = mockk() private val mockDataProvider: IChangelogDataProvider = mockk() - private val changelogRepository = - ChangelogRepository(preferences = mockedPreferences, dataProvider = mockDataProvider) + private val changelogRepository = ChangelogRepository(dataProvider = mockDataProvider) @Test fun `when given a changelog text should return a list of correctly formatted strings`() { diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCaseTest.kt index 1aead39d85..e9452884cf 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCaseTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCaseTest.kt @@ -29,7 +29,7 @@ class VersionNotificationUseCaseTest { @BeforeEach fun setup() { MockKAnnotations.init(this) - every { mockAppVersionInfoRepository.versionInfo() } returns versionInfo + every { mockAppVersionInfoRepository.versionInfo } returns versionInfo versionNotificationUseCase = VersionNotificationUseCase( diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt deleted file mode 100644 index 7888f02a4d..0000000000 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt +++ /dev/null @@ -1,78 +0,0 @@ -package net.mullvad.mullvadvpn.viewmodel - -import app.cash.turbine.test -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.just -import io.mockk.unmockkAll -import kotlin.test.assertEquals -import kotlinx.coroutines.test.runTest -import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule -import net.mullvad.mullvadvpn.lib.model.BuildVersion -import net.mullvad.mullvadvpn.repository.ChangelogRepository -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith - -@ExtendWith(TestCoroutineRule::class) -class ChangelogViewModelTest { - - @MockK private lateinit var mockedChangelogRepository: ChangelogRepository - - private lateinit var viewModel: ChangelogViewModel - - private val buildVersion = BuildVersion("1.0", 10) - - @BeforeEach - fun setup() { - MockKAnnotations.init(this) - every { mockedChangelogRepository.setVersionCodeOfMostRecentChangelogShowed(any()) } just - Runs - } - - @AfterEach - fun teardown() { - unmockkAll() - } - - @Test - fun `given up to date version code uiSideEffect should not emit`() = runTest { - // Arrange - every { mockedChangelogRepository.getVersionCodeOfMostRecentChangelogShowed() } returns - buildVersion.code - viewModel = ChangelogViewModel(mockedChangelogRepository, buildVersion, false) - - // If we have the most up to date version code, we should not show the changelog dialog - viewModel.uiSideEffect.test { expectNoEvents() } - } - - @Test - fun `given old version code uiSideEffect should emit ChangeLog`() = runTest { - // Arrange - val version = -1 - val changes = listOf("first change", "second change") - every { mockedChangelogRepository.getVersionCodeOfMostRecentChangelogShowed() } returns - version - every { mockedChangelogRepository.getLastVersionChanges() } returns changes - - viewModel = ChangelogViewModel(mockedChangelogRepository, buildVersion, false) - // Given a new version with a change log we should return it - viewModel.uiSideEffect.test { - assertEquals(awaitItem(), Changelog(version = buildVersion.name, changes = changes)) - } - } - - @Test - fun `given old version code and empty change log uiSideEffect should not emit`() = runTest { - // Arrange - every { mockedChangelogRepository.getVersionCodeOfMostRecentChangelogShowed() } returns -1 - every { mockedChangelogRepository.getLastVersionChanges() } returns emptyList() - - viewModel = ChangelogViewModel(mockedChangelogRepository, buildVersion, false) - // Given a new version with a change log we should not return it - viewModel.uiSideEffect.test { expectNoEvents() } - } -} 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 9696a30539..33e836acd8 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 @@ -124,7 +124,9 @@ class ConnectViewModelTest { connectionProxy = mockConnectionProxy, lastKnownLocationUseCase = mockLastKnownLocationUseCase, vpnPermissionRepository = mockVpnPermissionRepository, + resources = mockk(), isPlayBuild = false, + packageName = "net.mullvad.mullvadvpn", ) } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt index 97259b8b43..8857eb364a 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt @@ -35,7 +35,7 @@ class SettingsViewModelTest { val deviceState = MutableStateFlow<DeviceState>(DeviceState.LoggedOut) every { mockDeviceRepository.deviceState } returns deviceState - every { mockAppVersionInfoRepository.versionInfo() } returns versionInfo + every { mockAppVersionInfoRepository.versionInfo } returns versionInfo viewModel = SettingsViewModel( diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index 693ac3a789..964ad4c3f2 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -416,4 +416,7 @@ <string name="collapse">Collapse</string> <string name="copy">Copy</string> <string name="share">Share…</string> + <string name="app_info">App info</string> + <string name="changelog_title">Changelog</string> + <string name="version">Version</string> </resources> diff --git a/android/lib/resource/src/main/res/values/strings_non_translatable.xml b/android/lib/resource/src/main/res/values/strings_non_translatable.xml index 9cf571171a..0f0a64e796 100644 --- a/android/lib/resource/src/main/res/values/strings_non_translatable.xml +++ b/android/lib/resource/src/main/res/values/strings_non_translatable.xml @@ -4,6 +4,7 @@ <string name="voucher_hint" translatable="false">XXXX-XXXX-XXXX-XXXX</string> <string name="account_url" translatable="false">https://mullvad.net/account</string> <string name="download_url" translatable="false">https://mullvad.net/download/vpn/android</string> + <string name="market_uri" translatable="false">market://details?id=%s</string> <string name="faqs_and_guides_url" translatable="false">https://mullvad.net/help/tag/mullvad-app/</string> <string name="privacy_policy_url" translatable="false">https://mullvad.net/help/privacy-policy/</string> <string name="lockdown_url" translatable="false">https://mullvad.net/l/android-lockdown</string> |
