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 0e5b01b
Show file tree
Hide file tree
Showing 59 changed files with 1,327 additions and 133 deletions.
3 changes: 3 additions & 0 deletions android/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ Line wrap the file at 100 chars. Th
proxies. The access method is enabled by default.
- Add multihop which allows the routing of traffic through an entry and exit server, making it
harder to trace.
- Enable DAITA to route traffic through servers with DAITA support to enable the use
of all servers together with DAITA. This behaviour can be disabled with the use of the
"Direct only" setting.

### Changed
- Animation has been changed to look better with predictive back.
Expand Down
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 = { DaitaPages.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 = DaitaPages.entries.size,
) { page ->
Column(modifier = Modifier.fillMaxWidth()) {
val page = DaitaPages.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 DaitaPages(
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 0e5b01b

Please sign in to comment.