diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreenTest.kt new file mode 100644 index 000000000000..7513597754e9 --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreenTest.kt @@ -0,0 +1,157 @@ +package net.mullvad.mullvadvpn.compose.screen + +import androidx.compose.runtime.Composable +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import io.mockk.MockKAnnotations +import io.mockk.mockk +import io.mockk.verify +import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension +import net.mullvad.mullvadvpn.compose.setContentWithTheme +import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDES_IMPORT_BY_FILE_TEST_TAG +import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDES_IMPORT_BY_TEXT_TEST_TAG +import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDE_IMPORT_TEST_TAG +import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDE_INFO_TEST_TAG +import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDE_MORE_VERT_TEST_TAG +import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDE_RESET_OVERRIDES_TEST_TAG +import net.mullvad.mullvadvpn.viewmodel.ServerIpOverridesViewState +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +@ExperimentalTestApi +class ServerIpOverridesScreenTest { + @JvmField @RegisterExtension val composeExtension = createEdgeToEdgeComposeExtension() + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + } + + @Composable + private fun Screen( + state: ServerIpOverridesViewState, + onBackClick: () -> Unit = {}, + onInfoClick: () -> Unit = {}, + onResetOverridesClick: () -> Unit = {}, + onImportByFile: () -> Unit = {}, + onImportByText: () -> Unit = {}, + ) { + ServerIpOverridesScreen( + state = state, + onBackClick = onBackClick, + onInfoClick = onInfoClick, + onResetOverridesClick = onResetOverridesClick, + onImportByFile = onImportByFile, + onImportByText = onImportByText + ) + } + + @Test + fun testOverridesInactive() = + composeExtension.use { + // Arrange + setContentWithTheme { Screen(state = ServerIpOverridesViewState(false)) } + + // Assert + onNodeWithText("Overrides inactive").assertExists() + } + + @Test + fun testOverridesActive() = + composeExtension.use { + // Arrange + setContentWithTheme { Screen(state = ServerIpOverridesViewState(true)) } + + // Assert + onNodeWithText("Overrides active").assertExists() + } + + @Test + fun testOverridesActiveShowsWarningOnImport() = + composeExtension.use { + // Arrange + setContentWithTheme { Screen(state = ServerIpOverridesViewState(true)) } + + // Act + onNodeWithTag(testTag = SERVER_IP_OVERRIDE_IMPORT_TEST_TAG).performClick() + + // Assert + onNodeWithText( + "Importing new overrides might replace some previously imported overrides." + ) + .assertExists() + } + + @Test + fun testInfoClick() = + composeExtension.use { + // Arrange + val clickHandler: () -> Unit = mockk(relaxed = true) + setContentWithTheme { + Screen(state = ServerIpOverridesViewState(false), onInfoClick = clickHandler) + } + + // Act + onNodeWithTag(SERVER_IP_OVERRIDE_INFO_TEST_TAG).performClick() + + // Assert + verify { clickHandler() } + } + + @Test + fun testResetClick() = + composeExtension.use { + // Arrange + val clickHandler: () -> Unit = mockk(relaxed = true) + setContentWithTheme { + Screen( + state = ServerIpOverridesViewState(false), + onResetOverridesClick = clickHandler + ) + } + + // Act + onNodeWithTag(SERVER_IP_OVERRIDE_MORE_VERT_TEST_TAG).performClick() + onNodeWithTag(SERVER_IP_OVERRIDE_RESET_OVERRIDES_TEST_TAG).performClick() + + // Assert + verify { clickHandler() } + } + + @Test + fun testImportByFile() = + composeExtension.use { + // Arrange + val clickHandler: () -> Unit = mockk(relaxed = true) + setContentWithTheme { + Screen(state = ServerIpOverridesViewState(false), onImportByFile = clickHandler) + } + + // Act + onNodeWithTag(SERVER_IP_OVERRIDE_IMPORT_TEST_TAG).performClick() + onNodeWithTag(SERVER_IP_OVERRIDES_IMPORT_BY_FILE_TEST_TAG).performClick() + + // Assert + verify { clickHandler() } + } + + @Test + fun testImportByText() = + composeExtension.use { + // Arrange + val clickHandler: () -> Unit = mockk(relaxed = true) + setContentWithTheme { + Screen(state = ServerIpOverridesViewState(false), onImportByText = clickHandler) + } + + // Act + onNodeWithTag(SERVER_IP_OVERRIDE_IMPORT_TEST_TAG).performClick() + onNodeWithTag(SERVER_IP_OVERRIDES_IMPORT_BY_TEXT_TEST_TAG).performClick() + + // Assert + verify { clickHandler() } + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/InfoIconButton.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/InfoIconButton.kt index efd90b0ac770..5c28069c52fe 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/InfoIconButton.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/InfoIconButton.kt @@ -5,16 +5,22 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import net.mullvad.mullvadvpn.R @Composable -fun InfoIconButton(onClick: () -> Unit, modifier: Modifier = Modifier) { +fun InfoIconButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + contentDescription: String? = null, + iconTint: Color = MaterialTheme.colorScheme.onPrimary +) { IconButton(modifier = modifier, onClick = onClick) { Icon( painter = painterResource(id = R.drawable.icon_info), - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimary + contentDescription = contentDescription, + tint = iconTint ) } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/IconCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/IconCell.kt index faf537fb7f61..3b68e42e45bc 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/IconCell.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/IconCell.kt @@ -25,13 +25,14 @@ private fun PreviewIconCell() { @Composable fun IconCell( iconId: Int?, - contentDescription: String? = null, title: String, + modifier: Modifier = Modifier, + contentDescription: String? = null, titleStyle: TextStyle = MaterialTheme.typography.labelLarge, titleColor: Color = MaterialTheme.colorScheme.onPrimary, onClick: () -> Unit = {}, background: Color = MaterialTheme.colorScheme.primary, - enabled: Boolean = true, + enabled: Boolean = true ) { BaseCell( headlineContent = { @@ -49,6 +50,7 @@ fun IconCell( }, onCellClicked = onClick, background = background, - isRowEnabled = enabled + isRowEnabled = enabled, + modifier = modifier ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ServerIpOverridesCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ServerIpOverridesCell.kt index 12f500f6107c..acc1943498ff 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ServerIpOverridesCell.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ServerIpOverridesCell.kt @@ -2,18 +2,12 @@ package net.mullvad.mullvadvpn.compose.cell import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color @@ -39,43 +33,38 @@ fun ServerIpOverridesCell( activeColor: Color = MaterialTheme.colorScheme.selected, inactiveColor: Color = MaterialTheme.colorScheme.error, ) { - Row( - modifier = - modifier - .wrapContentHeight() - .height(IntrinsicSize.Min) - .background(MaterialTheme.colorScheme.primary) - .padding(horizontal = Dimens.sideMargin) - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Box( - modifier = - Modifier.size(Dimens.relayCircleSize) - .background( - color = - when { - active -> activeColor - else -> inactiveColor - }, - shape = CircleShape - ) - ) - Text( - text = - if (active) stringResource(id = R.string.server_ip_overrides_active) - else stringResource(id = R.string.server_ip_overrides_inactive), - color = MaterialTheme.colorScheme.onPrimary, - modifier = - Modifier.weight(1f) - .alpha( - if (active) { - AlphaVisible - } else { - AlphaInactive - } - ) - .padding(horizontal = Dimens.smallPadding, vertical = Dimens.mediumPadding) - ) - } + BaseCell( + iconView = { + Box( + modifier = + Modifier.size(Dimens.relayCircleSize) + .background( + color = + when { + active -> activeColor + else -> inactiveColor + }, + shape = CircleShape + ) + ) + }, + headlineContent = { + Text( + text = + if (active) stringResource(id = R.string.server_ip_overrides_active) + else stringResource(id = R.string.server_ip_overrides_inactive), + color = MaterialTheme.colorScheme.onPrimary, + modifier = + Modifier.weight(1f) + .alpha( + if (active) { + AlphaVisible + } else { + AlphaInactive + } + ) + .padding(horizontal = Dimens.smallPadding, vertical = Dimens.mediumPadding) + ) + } + ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ResetServerIpOverridesConfirmationDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ResetServerIpOverridesConfirmationDialog.kt index 30139f648fd4..25e32f8efe75 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ResetServerIpOverridesConfirmationDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ResetServerIpOverridesConfirmationDialog.kt @@ -1,7 +1,5 @@ package net.mullvad.mullvadvpn.compose.dialog -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme @@ -18,7 +16,6 @@ import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.button.NegativeButton import net.mullvad.mullvadvpn.compose.button.PrimaryButton import net.mullvad.mullvadvpn.lib.theme.AppTheme -import net.mullvad.mullvadvpn.lib.theme.Dimens @Preview @Composable @@ -34,30 +31,29 @@ fun ResetServerIpOverridesConfirmationDialog( AlertDialog( containerColor = MaterialTheme.colorScheme.background, confirmButton = { - Column(verticalArrangement = Arrangement.spacedBy(Dimens.buttonSpacing)) { - NegativeButton( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.server_ip_overrides_reset_reset_button), - onClick = { resultNavigator.navigateBack(result = true) } - ) - - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.cancel), - onClick = resultNavigator::navigateBack - ) - } + NegativeButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.server_ip_overrides_reset_reset_button), + onClick = { resultNavigator.navigateBack(result = true) } + ) + }, + dismissButton = { + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.cancel), + onClick = resultNavigator::navigateBack + ) }, title = { Text( text = stringResource(id = R.string.server_ip_overrides_reset_title), - color = MaterialTheme.colorScheme.onPrimary + color = MaterialTheme.colorScheme.onBackground ) }, text = { Text( text = stringResource(id = R.string.server_ip_overrides_reset_body), - color = MaterialTheme.colorScheme.onPrimary, + color = MaterialTheme.colorScheme.onBackground, style = MaterialTheme.typography.bodySmall, ) }, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreen.kt index 35f934295167..a7e802e89ce8 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreen.kt @@ -556,21 +556,16 @@ private fun CustomListsBottomSheet( IconCell( iconId = R.drawable.icon_add, title = stringResource(id = R.string.new_list), + titleColor = onBackgroundColor, onClick = { onCreateCustomList() closeBottomSheet(true) }, - background = Color.Unspecified, - titleColor = onBackgroundColor + background = Color.Unspecified ) IconCell( iconId = R.drawable.icon_edit, title = stringResource(id = R.string.edit_lists), - onClick = { - onEditCustomLists() - closeBottomSheet(true) - }, - background = Color.Unspecified, titleColor = onBackgroundColor.copy( alpha = @@ -580,6 +575,11 @@ private fun CustomListsBottomSheet( AlphaInactive } ), + onClick = { + onEditCustomLists() + closeBottomSheet(true) + }, + background = Color.Unspecified, enabled = bottomSheetState.editListEnabled ) } @@ -609,13 +609,6 @@ private fun LocationBottomSheet( customLists.forEach { val enabled = it.canAddLocation(item) IconCell( - background = Color.Unspecified, - titleColor = - if (enabled) { - onBackgroundColor - } else { - MaterialTheme.colorScheme.onSecondary - }, iconId = null, title = if (enabled) { @@ -623,22 +616,29 @@ private fun LocationBottomSheet( } else { stringResource(id = R.string.location_added, it.name) }, + titleColor = + if (enabled) { + onBackgroundColor + } else { + MaterialTheme.colorScheme.onSecondary + }, onClick = { onAddLocationToList(item, it) closeBottomSheet(true) }, + background = Color.Unspecified, enabled = enabled ) } IconCell( iconId = R.drawable.icon_add, title = stringResource(id = R.string.new_list), + titleColor = onBackgroundColor, onClick = { onCreateCustomList(item) closeBottomSheet(true) }, - background = Color.Unspecified, - titleColor = onBackgroundColor + background = Color.Unspecified ) } } @@ -662,33 +662,33 @@ private fun EditCustomListBottomSheet( IconCell( iconId = R.drawable.icon_edit, title = stringResource(id = R.string.edit_name), + titleColor = onBackgroundColor, onClick = { onEditName(customList) closeBottomSheet(true) }, - background = Color.Unspecified, - titleColor = onBackgroundColor + background = Color.Unspecified ) IconCell( iconId = R.drawable.icon_add, title = stringResource(id = R.string.edit_locations), + titleColor = onBackgroundColor, onClick = { onEditLocations(customList) closeBottomSheet(true) }, - background = Color.Unspecified, - titleColor = onBackgroundColor + background = Color.Unspecified ) HorizontalDivider(color = onBackgroundColor) IconCell( iconId = R.drawable.icon_delete, title = stringResource(id = R.string.delete), + titleColor = onBackgroundColor, onClick = { onDeleteCustomList(customList) closeBottomSheet(true) }, - background = Color.Unspecified, - titleColor = onBackgroundColor + background = Color.Unspecified ) } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreen.kt index 34e4d18fe6a1..873a30d96630 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreen.kt @@ -1,7 +1,6 @@ package net.mullvad.mullvadvpn.compose.screen import android.content.Context -import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.animateContentSize @@ -36,6 +35,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -57,10 +57,16 @@ import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar import net.mullvad.mullvadvpn.compose.destinations.ImportOverridesByTextDestination import net.mullvad.mullvadvpn.compose.destinations.ResetServerIpOverridesConfirmationDialogDestination import net.mullvad.mullvadvpn.compose.destinations.ServerIpOverridesInfoDialogDestination +import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDES_IMPORT_BY_FILE_TEST_TAG +import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDES_IMPORT_BY_TEXT_TEST_TAG +import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDE_IMPORT_TEST_TAG +import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDE_INFO_TEST_TAG +import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDE_MORE_VERT_TEST_TAG +import net.mullvad.mullvadvpn.compose.test.SERVER_IP_OVERRIDE_RESET_OVERRIDES_TEST_TAG import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightLeafTransition import net.mullvad.mullvadvpn.compose.util.LaunchedEffectCollect import net.mullvad.mullvadvpn.compose.util.OnNavResultValue -import net.mullvad.mullvadvpn.compose.util.showSnackBarDirect +import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.lib.theme.color.AlphaDisabled @@ -75,12 +81,12 @@ private fun PreviewServerIpOverridesScreen() { AppTheme { ServerIpOverridesScreen( ServerIpOverridesViewState(false), - SnackbarHostState(), onBackClick = {}, onInfoClick = {}, onResetOverridesClick = {}, onImportByFile = {}, - onImportByText = {} + onImportByText = {}, + SnackbarHostState() ) } } @@ -101,13 +107,13 @@ fun ServerIpOverrides( LaunchedEffectCollect(vm.uiSideEffect) { sideEffect -> when (sideEffect) { is ServerIpOverridesViewModel.UiSideEffect.ImportResult -> - snackbarHostState.showSnackBarDirect( + snackbarHostState.showSnackbarImmediately( this, message = sideEffect.error.toString(context), actionLabel = null ) ServerIpOverridesViewModel.UiSideEffect.OverridesCleared -> - snackbarHostState.showSnackBarDirect( + snackbarHostState.showSnackbarImmediately( this, message = context.getString(R.string.overrides_cleared), actionLabel = null @@ -127,7 +133,6 @@ fun ServerIpOverrides( ServerIpOverridesScreen( state, - snackbarHostState, onBackClick = navigator::navigateUp, onInfoClick = { navigator.navigate(ServerIpOverridesInfoDialogDestination, onlyIfResumed = true) @@ -141,7 +146,8 @@ fun ServerIpOverrides( onImportByFile = { openFileLauncher.launch("application/json") }, onImportByText = { navigator.navigate(ImportOverridesByTextDestination, onlyIfResumed = true) - } + }, + snackbarHostState ) } @@ -149,12 +155,12 @@ fun ServerIpOverrides( @Composable fun ServerIpOverridesScreen( state: ServerIpOverridesViewState, - snackbarHostState: SnackbarHostState, onBackClick: () -> Unit, onInfoClick: () -> Unit, onResetOverridesClick: () -> Unit, onImportByFile: () -> Unit, - onImportByText: () -> Unit + onImportByText: () -> Unit, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() } ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -169,7 +175,7 @@ fun ServerIpOverridesScreen( }, actions = { TopBarActions( - state.overridesActive, + overridesActive = state.overridesActive, onInfoClick = onInfoClick, onResetOverridesClick = onResetOverridesClick ) @@ -196,7 +202,8 @@ fun ServerIpOverridesScreen( text = stringResource(R.string.server_ip_overrides_import_button), modifier = Modifier.padding(horizontal = Dimens.sideMargin) - .padding(bottom = Dimens.screenVerticalMargin), + .padding(bottom = Dimens.screenVerticalMargin) + .testTag(SERVER_IP_OVERRIDE_IMPORT_TEST_TAG), ) SnackbarHost(hostState = snackbarHostState, modifier = Modifier.animateContentSize()) { MullvadSnackbar(snackbarData = it) @@ -227,10 +234,7 @@ private fun ImportOverridesByBottomSheet( MullvadModalBottomSheet( sheetState = sheetState, - onDismissRequest = { - Log.d("ServerIpOverridesScreen", "onDismissRequest") - showBottomSheet(false) - }, + onDismissRequest = { showBottomSheet(false) }, ) { -> HeaderCell( text = stringResource(id = R.string.server_ip_overrides_import_by), @@ -245,6 +249,7 @@ private fun ImportOverridesByBottomSheet( onCloseSheet() }, background = Color.Unspecified, + modifier = Modifier.testTag(SERVER_IP_OVERRIDES_IMPORT_BY_FILE_TEST_TAG) ) IconCell( iconId = R.drawable.icon_text_fields, @@ -254,6 +259,7 @@ private fun ImportOverridesByBottomSheet( onCloseSheet() }, background = Color.Unspecified, + modifier = Modifier.testTag(SERVER_IP_OVERRIDES_IMPORT_BY_TEXT_TEST_TAG) ) if (overridesActive) { HorizontalDivider(color = MaterialTheme.colorScheme.onBackground) @@ -290,8 +296,14 @@ private fun TopBarActions( onResetOverridesClick: () -> Unit ) { var showMenu by remember { mutableStateOf(false) } - InfoIconButton(onClick = onInfoClick) - IconButton(onClick = { showMenu = !showMenu }) { + InfoIconButton( + onClick = onInfoClick, + modifier = Modifier.testTag(SERVER_IP_OVERRIDE_INFO_TEST_TAG) + ) + IconButton( + onClick = { showMenu = !showMenu }, + modifier = Modifier.testTag(SERVER_IP_OVERRIDE_MORE_VERT_TEST_TAG) + ) { Icon(painterResource(id = R.drawable.icon_more_vert), contentDescription = null) } DropdownMenu( @@ -317,7 +329,8 @@ private fun TopBarActions( Icons.Filled.Delete, contentDescription = null, ) - } + }, + modifier = Modifier.testTag(SERVER_IP_OVERRIDE_RESET_OVERRIDES_TEST_TAG) ) } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt index efd8e34250d1..8d45f1a6f076 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt @@ -64,3 +64,10 @@ const val SELECT_LOCATION_CUSTOM_LIST_BOTTOM_SHEET_TEST_TAG = "select_location_custom_list_bottom_sheet_test_tag" const val SELECT_LOCATION_LOCATION_BOTTOM_SHEET_TEST_TAG = "select_location_location_bottom_sheet_test_tag" + +const val SERVER_IP_OVERRIDE_IMPORT_TEST_TAG = "server_ip_override_import_button_test_tag" +const val SERVER_IP_OVERRIDE_INFO_TEST_TAG = "server_ip_override_info_button_test_tag" +const val SERVER_IP_OVERRIDE_MORE_VERT_TEST_TAG = "server_ip_override_more_vert_button_test_tag" +const val SERVER_IP_OVERRIDE_RESET_OVERRIDES_TEST_TAG = "server_ip_override_reset_button_test_tag" +const val SERVER_IP_OVERRIDES_IMPORT_BY_FILE_TEST_TAG = "server_ip_override_import_by_file_test_tag" +const val SERVER_IP_OVERRIDES_IMPORT_BY_TEXT_TEST_TAG = "server_ip_override_import_by_text_test_tag" diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Snackbar.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Snackbar.kt index 61b90fb52eaa..3e5b7e16187e 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Snackbar.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Snackbar.kt @@ -5,7 +5,7 @@ import androidx.compose.material3.SnackbarHostState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -suspend fun SnackbarHostState.showSnackBarDirect( +suspend fun SnackbarHostState.showSnackbarImmediately( coroutineScope: CoroutineScope, message: String, actionLabel: String? = null, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt index d223ece266ef..7d61feaf0cf2 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt @@ -32,7 +32,7 @@ import net.mullvad.mullvadvpn.util.flatMapReadyConnectionOrDefault class SettingsRepository( private val serviceConnectionManager: ServiceConnectionManager, private val messageHandler: MessageHandler, - dispatcher: CoroutineDispatcher = Dispatchers.IO, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO, ) { val settingsUpdates: StateFlow = serviceConnectionManager.connectionState @@ -102,7 +102,7 @@ class SettingsRepository( } suspend fun applySettingsPatch(json: String) = - withContext(Dispatchers.IO) { + withContext(dispatcher) { val deferred = async { messageHandler.events().first() } messageHandler.trySendRequest(Request.ApplyJsonSettings(json)) deferred.await() diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt index 85dfb0f99725..c7a9be2ff9bd 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt @@ -96,7 +96,7 @@ class MainActivity : ComponentActivity() { super.onActivityResult(requestCode, resultCode, resultData) // Ensure we are responding to the correct request - if (requestCode == 0) { + if (requestCode == REQUEST_VPN_PERMISSION_RESULT_CODE) { serviceConnectionManager.onVpnPermissionResult(resultCode == Activity.RESULT_OK) } } @@ -117,6 +117,10 @@ class MainActivity : ComponentActivity() { private fun requestVpnPermission() { val intent = VpnService.prepare(this) - startActivityForResult(intent, 0) + startActivityForResult(intent, REQUEST_VPN_PERMISSION_RESULT_CODE) + } + + companion object { + private const val REQUEST_VPN_PERMISSION_RESULT_CODE = 0 } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt index b5c9d606349d..2bb40cd4f2f5 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt @@ -53,7 +53,9 @@ class ServerIpOverridesViewModel( fun importText(config: String) = viewModelScope.launch { applySettingsPatch(config) } private suspend fun applySettingsPatch(json: String) { - // Wait for daemon to come online since we might be paused + // Wait for daemon to come online since we might be disconnected (due to File picker being + // open + // and we disconnect from daemon in paused state) val connResult = withTimeoutOrNull(5.seconds) { serviceConnectionManager.connectionState diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index e8aaa197cdb8..4c8da54d816d 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -336,9 +336,9 @@ Import via text Importing new overrides might replace some previously imported overrides. Patch not matching specification - Invalid or missing value: \'%1$s\' + Invalid or missing value: \"%1$s\" Unable to parse patch - Unknown or prohibited key \'%1$s\' + Unknown or prohibited key \"%1$s\" Failed to apply patch Recursion limit Success diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index 10cd401b4cc4..dfa09f452561 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -2181,7 +2181,7 @@ msgstr "" msgid "Install Mullvad VPN (%s) to stay up to date" msgstr "" -msgid "Invalid or missing value: '%s'" +msgid "Invalid or missing value: \"%s\"" msgstr "" msgid "List name" @@ -2343,7 +2343,7 @@ msgstr "" msgid "Undo" msgstr "" -msgid "Unknown or prohibited key '%s'" +msgid "Unknown or prohibited key \"%s\"" msgstr "" msgid "Unsecured"