Skip to content

Commit

Permalink
Implement support for daita with multihop
Browse files Browse the repository at this point in the history
  • Loading branch information
Pururun committed Nov 25, 2024
1 parent 5352c5c commit bb69a34
Show file tree
Hide file tree
Showing 34 changed files with 1,186 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ object ContentType {
const val SPACER = 5
const val PROGRESS = 6
const val EMPTY_TEXT = 7
const val BUTTON = 8
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package net.mullvad.mullvadvpn.compose.dialog

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
Expand All @@ -18,34 +16,24 @@ import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.dialog.info.InfoConfirmationDialog
import net.mullvad.mullvadvpn.compose.dialog.info.InfoConfirmationDialogTitleType
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens

@Preview
@Composable
private fun PreviewDaitaConfirmationDialog() {
AppTheme { DaitaConfirmation(EmptyResultBackNavigator()) }
private fun PreviewDaitaDirectOnlyConfirmationDialog() {
AppTheme { DaitaDirectOnlyConfirmation(EmptyResultBackNavigator()) }
}

@Destination<RootGraph>(style = DestinationStyle.Dialog::class)
@Composable
fun DaitaConfirmation(navigator: ResultBackNavigator<Boolean>) {
fun DaitaDirectOnlyConfirmation(navigator: ResultBackNavigator<Boolean>) {
InfoConfirmationDialog(
navigator = navigator,
titleType = InfoConfirmationDialogTitleType.IconOnly,
confirmButtonTitle = stringResource(R.string.enable_anyway),
cancelButtonTitle = stringResource(R.string.back),
confirmButtonTitle = stringResource(R.string.enable_direct_only),
cancelButtonTitle = stringResource(R.string.cancel),
) {
Text(
text = stringResource(id = R.string.daita_relay_subset_warning),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.fillMaxWidth(),
)

Spacer(modifier = Modifier.height(Dimens.verticalSpace))

Text(
text = stringResource(id = R.string.daita_warning, stringResource(id = R.string.daita)),
text = stringResource(id = R.string.direct_only_description),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.fillMaxWidth(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,15 @@ import net.mullvad.mullvadvpn.lib.theme.AppTheme

@Preview
@Composable
private fun PreviewDaitaInfoDialog() {
AppTheme { DaitaInfo(EmptyDestinationsNavigator) }
private fun PreviewDaitaDirectOnlyInfoDialog() {
AppTheme { DaitaDirectOnlyInfo(EmptyDestinationsNavigator) }
}

@Destination<RootGraph>(style = DestinationStyle.Dialog::class)
@Composable
fun DaitaInfo(navigator: DestinationsNavigator) {
fun DaitaDirectOnlyInfo(navigator: DestinationsNavigator) {
InfoDialog(
message =
stringResource(
id = R.string.daita_info,
stringResource(id = R.string.daita),
stringResource(id = R.string.daita_full),
),
additionalInfo =
stringResource(id = R.string.daita_warning, stringResource(id = R.string.daita)),
message = stringResource(id = R.string.daita_info),
onDismiss = dropUnlessResumed { navigator.navigateUp() },
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ class SettingsUiStatePreviewParameterProvider : PreviewParameterProvider<Setting
appVersion = "2222.22",
isLoggedIn = true,
isSupportedVersion = true,
isDaitaEnabled = true,
isPlayBuild = true,
multihopEnabled = false,
),
SettingsUiState(
appVersion = "9000.1",
isLoggedIn = false,
isSupportedVersion = false,
isDaitaEnabled = false,
isPlayBuild = false,
multihopEnabled = false,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ class VpnSettingsUiStatePreviewParameterProvider : PreviewParameterProvider<VpnS
VpnSettingsUiState.createDefault(
mtu = Mtu(MTU),
isLocalNetworkSharingEnabled = true,
isDaitaEnabled = true,
isCustomDnsEnabled = true,
customDnsItems = listOf(CustomDnsItem("0.0.0.0", false)),
contentBlockersOptions =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package net.mullvad.mullvadvpn.compose.screen

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
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.DaitaDirectOnlyConfirmationDestination
import com.ramcosta.composedestinations.generated.destinations.DaitaDirectOnlyInfoDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.cell.HeaderSwitchComposeCell
import net.mullvad.mullvadvpn.compose.cell.SwitchComposeSubtitleCell
import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar
import net.mullvad.mullvadvpn.compose.state.DaitaUiState
import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition
import net.mullvad.mullvadvpn.compose.util.OnNavResultValue
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.viewmodel.DaitaViewModel
import org.koin.androidx.compose.koinViewModel

@Preview
@Composable
private fun PreviewDaitaScreen() {
AppTheme { DaitaScreen(state = DaitaUiState(daitaEnabled = false, directOnly = false)) }
}

@Destination<RootGraph>(style = SlideInFromRightTransition::class)
@Composable
fun Daita(
navigator: DestinationsNavigator,
daitaConfirmationDialogResult: ResultRecipient<DaitaDirectOnlyConfirmationDestination, Boolean>,
) {
val viewModel = koinViewModel<DaitaViewModel>()
val state by viewModel.uiState.collectAsStateWithLifecycle()

daitaConfirmationDialogResult.OnNavResultValue {
if (it) {
viewModel.setDirectOnly(true)
}
}

DaitaScreen(
state = state,
onDaitaEnabled = viewModel::setDaita,
onDirectOnly = { enable ->
if (enable) {
navigator.navigate(DaitaDirectOnlyConfirmationDestination)
} else {
viewModel.setDirectOnly(false)
}
},
onDirectOnlyInfoClick =
dropUnlessResumed { navigator.navigate(DaitaDirectOnlyInfoDestination) },
onBackClick = dropUnlessResumed { navigator.navigateUp() },
)
}

@Composable
fun DaitaScreen(
state: DaitaUiState,
onDaitaEnabled: (enable: Boolean) -> Unit = {},
onDirectOnly: (enable: Boolean) -> Unit = {},
onDirectOnlyInfoClick: () -> Unit = {},
onBackClick: () -> Unit = {},
) {
ScaffoldWithMediumTopBar(
appBarTitle = stringResource(id = R.string.daita),
navigationIcon = { NavigateBackIconButton { onBackClick() } },
) { modifier ->
Column(modifier = modifier) {
val pagerState = rememberPagerState(pageCount = { DAITA_PAGES.entries.size })
DescriptionPager(pagerState = pagerState)
PageIndicator(pagerState = pagerState)
HeaderSwitchComposeCell(
title = stringResource(R.string.enable),
isToggled = state.daitaEnabled,
onCellClicked = onDaitaEnabled,
)
HorizontalDivider()
HeaderSwitchComposeCell(
title = stringResource(R.string.direct_only),
isToggled = state.directOnly,
isEnabled = state.daitaEnabled,
onCellClicked = onDirectOnly,
onInfoClicked = onDirectOnlyInfoClick,
)
}
}
}

@Composable
private fun DescriptionPager(pagerState: PagerState) {
HorizontalPager(
state = pagerState,
verticalAlignment = Alignment.Top,
beyondViewportPageCount = DAITA_PAGES.entries.size,
) { page ->
Column(modifier = Modifier.fillMaxWidth()) {
val page = DAITA_PAGES.entries[page]
// Scale image to fit width up to certain width
Image(
contentScale = ContentScale.FillWidth,
modifier =
Modifier.widthIn(max = Dimens.settingsDetailsImageMaxWidth)
.fillMaxWidth()
.padding(horizontal = Dimens.mediumPadding)
.align(Alignment.CenterHorizontally),
painter = painterResource(id = page.image),
contentDescription = stringResource(R.string.daita),
)
DescriptionText(
firstParagraph = page.textFirstParagraph,
secondParagraph = page.textSecondParagraph,
thirdParagraph = page.textThirdParagraph,
)
}
}
}

@Composable
private fun DescriptionText(firstParagraph: Int, secondParagraph: Int, thirdParagraph: Int) {
SwitchComposeSubtitleCell(
modifier = Modifier.padding(vertical = Dimens.smallPadding),
text =
buildString {
appendLine(stringResource(firstParagraph))
appendLine()
appendLine(stringResource(secondParagraph))
appendLine()
append(stringResource(thirdParagraph))
},
)
}

@Composable
private fun PageIndicator(pagerState: PagerState) {
Row(
Modifier.wrapContentHeight().fillMaxWidth().padding(bottom = Dimens.mediumPadding),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Bottom,
) {
repeat(pagerState.pageCount) { iteration ->
val color =
if (pagerState.currentPage == iteration) MaterialTheme.colorScheme.onPrimary
else MaterialTheme.colorScheme.primary
Box(
modifier =
Modifier.padding(Dimens.indicatorPadding)
.clip(CircleShape)
.background(color)
.size(Dimens.indicatorSize)
)
}
}
}

private enum class DAITA_PAGES(
val image: Int,
val textFirstParagraph: Int,
val textSecondParagraph: Int,
val textThirdParagraph: Int,
) {
FIRST(
image = R.drawable.daita_illustration_1,
textFirstParagraph = R.string.daita_description_slide_1_first_paragraph,
textSecondParagraph = R.string.daita_description_slide_1_second_paragraph,
textThirdParagraph = R.string.daita_description_slide_1_third_paragraph,
),
SECOND(
image = R.drawable.daita_illustration_2,
textFirstParagraph = R.string.daita_description_slide_2_first_paragraph,
textSecondParagraph = R.string.daita_description_slide_2_second_paragraph,
textThirdParagraph = R.string.daita_description_slide_2_third_paragraph,
),
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ 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.DaitaDestination
import com.ramcosta.composedestinations.generated.destinations.MultihopDestination
import com.ramcosta.composedestinations.generated.destinations.ReportProblemDestination
import com.ramcosta.composedestinations.generated.destinations.SplitTunnelingDestination
Expand Down Expand Up @@ -74,6 +75,7 @@ fun Settings(navigator: DestinationsNavigator) {
onReportProblemCellClick =
dropUnlessResumed { navigator.navigate(ReportProblemDestination) },
onMultihopClick = dropUnlessResumed { navigator.navigate(MultihopDestination) },
onDaitaClick = dropUnlessResumed { navigator.navigate(DaitaDestination) },
onBackClick = dropUnlessResumed { navigator.navigateUp() },
)
}
Expand All @@ -88,6 +90,7 @@ fun SettingsScreen(
onReportProblemCellClick: () -> Unit = {},
onApiAccessClick: () -> Unit = {},
onMultihopClick: () -> Unit = {},
onDaitaClick: () -> Unit = {},
onBackClick: () -> Unit = {},
) {
ScaffoldWithMediumTopBar(
Expand All @@ -99,6 +102,9 @@ fun SettingsScreen(
state = lazyListState,
) {
if (state.isLoggedIn) {
itemWithDivider {
DaitaCell(isDaitaEnabled = state.isDaitaEnabled, onDaitaClick = onDaitaClick)
}
itemWithDivider {
MultihopCell(
isMultihopEnabled = state.multihopEnabled,
Expand Down Expand Up @@ -220,6 +226,23 @@ private fun PrivacyPolicy(state: SettingsUiState) {
)
}

@Composable
private fun DaitaCell(isDaitaEnabled: Boolean, onDaitaClick: () -> Unit) {
val title = stringResource(id = R.string.daita)
TwoRowCell(
titleText = title,
subtitleText =
stringResource(
if (isDaitaEnabled) {
R.string.on
} else {
R.string.off
}
),
onCellClicked = onDaitaClick,
)
}

@Composable
private fun MultihopCell(isMultihopEnabled: Boolean, onMultihopClick: () -> Unit) {
val title = stringResource(id = R.string.multihop)
Expand Down
Loading

0 comments on commit bb69a34

Please sign in to comment.