diff options
| author | Kalle Lindström <karl.lindstrom@mullvad.net> | 2025-07-22 14:26:22 +0200 |
|---|---|---|
| committer | Kalle Lindström <karl.lindstrom@mullvad.net> | 2025-07-22 14:26:22 +0200 |
| commit | b2fc803af349205bc40d7cd00e0a480536c3d09e (patch) | |
| tree | d603241a7e9ed6284f89704140f02c1a828518cb /android/lib | |
| parent | 75501a665b1bb7257cacd79f1eca84c839929725 (diff) | |
| parent | 526ecbf7d85c8abe7af08daf04dc4bc0c6df109c (diff) | |
| download | mullvadvpn-b2fc803af349205bc40d7cd00e0a480536c3d09e.tar.xz mullvadvpn-b2fc803af349205bc40d7cd00e0a480536c3d09e.zip | |
Merge branch 'implement-recents-support-ui'
Diffstat (limited to 'android/lib')
7 files changed, 186 insertions, 65 deletions
diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt index cff4e2bd3e..3716e4d9c0 100644 --- a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt +++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt @@ -576,6 +576,28 @@ class ManagementService( .mapLeft(SetRelayLocationError::Unknown) .mapEmpty() + suspend fun setRelayLocationMultihop( + entry: RelayItemId, + exit: RelayItemId, + ): Either<SetRelayLocationError, Unit> = + Either.catch { + val currentRelaySettings = getSettings().relaySettings + + val updatedRelaySettings = + currentRelaySettings.copy { + inside(RelaySettings.relayConstraints) { + RelayConstraints.location set Constraint.Only(exit) + RelayConstraints.wireguardConstraints.entryLocation set + Constraint.Only(entry) + RelayConstraints.wireguardConstraints.isMultihopEnabled set true + } + } + grpc.setRelaySettings(updatedRelaySettings.fromDomain()) + } + .onLeft { Logger.e("Set relay multihop error") } + .mapLeft(SetRelayLocationError::Unknown) + .mapEmpty() + suspend fun createCustomList( name: CustomListName, locations: List<GeoLocationId> = emptyList(), @@ -855,6 +877,11 @@ class ManagementService( .mapLeft(SetDaitaSettingsError::Unknown) .mapEmpty() + suspend fun setRecentsEnabled(enabled: Boolean): Either<SetWireguardConstraintsError, Unit> = + Either.catch { grpc.setEnableRecents(BoolValue.of(enabled)) } + .mapLeft(SetWireguardConstraintsError::Unknown) + .mapEmpty() + private fun <A> Either<A, Empty>.mapEmpty() = map {} private inline fun <B, C> Either<Throwable, B>.mapLeftStatus( diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt index 3ff0788776..27ce80c016 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt @@ -4,6 +4,31 @@ import arrow.optics.optics typealias DomainCustomList = CustomList +sealed interface Hop { + data class Single<R : RelayItem>(val relay: R) : Hop + + data class Multi(val entry: RelayItem, val exit: RelayItem) : Hop + + val isActive: Boolean + get() = + when (this) { + is Multi -> entry.active && exit.active + is Single<*> -> relay.active + } + + fun entry(): RelayItem = + when (this) { + is Multi -> entry + is Single<*> -> relay + } + + fun exit(): RelayItem = + when (this) { + is Multi -> exit + is Single<*> -> relay + } +} + @optics sealed interface RelayItem { val id: RelayItemId diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index fd01cfbcd6..abff8ca10c 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -427,4 +427,10 @@ <string name="app_is_blocking_internet">The app is blocking internet, please disconnect first</string> <string name="in_app_products_unavailable">In-app products unavailable, please make sure you have the latest version of Google Play.</string> <string name="relayitem_is_inactive">%s is unavailable</string> + <string name="recents">Recents</string> + <string name="enable_recents">Enable recents</string> + <string name="disable_recents">Disable recents</string> + <string name="no_recent_selection">No recent selection history</string> + <string name="recents_disabled">Recents disabled and history cleared</string> + <string name="more_actions">More actions</string> </resources> diff --git a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayListItem.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayListItem.kt index 8132a9ece7..f88b7b92b4 100644 --- a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayListItem.kt +++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayListItem.kt @@ -1,8 +1,11 @@ package net.mullvad.mullvadvpn.lib.ui.component.relaylist +import android.content.Context import net.mullvad.mullvadvpn.lib.model.CustomListId import net.mullvad.mullvadvpn.lib.model.CustomListName +import net.mullvad.mullvadvpn.lib.model.Hop import net.mullvad.mullvadvpn.lib.model.RelayItem +import net.mullvad.mullvadvpn.lib.resource.R enum class RelayListItemContentType { CUSTOM_LIST_HEADER, @@ -13,6 +16,10 @@ enum class RelayListItemContentType { LOCATION_ITEM, LOCATIONS_EMPTY_TEXT, EMPTY_RELAY_LIST, + RECENT_LIST_ITEM, + RECENT_LIST_HEADER, + RECENT_LIST_FOOTER, + SECTION_DIVIDER, } enum class RelayListItemState { @@ -24,46 +31,51 @@ sealed interface RelayListItem { val key: Any val contentType: RelayListItemContentType - data object CustomListHeader : RelayListItem { - override val key = "custom_list_header" - override val contentType = RelayListItemContentType.CUSTOM_LIST_HEADER - } - sealed interface SelectableItem : RelayListItem { - val item: RelayItem + val hop: Hop val depth: Int val isSelected: Boolean val expanded: Boolean + val canExpand: Boolean val state: RelayListItemState? val itemPosition: ItemPosition } + data object CustomListHeader : RelayListItem { + override val key = "custom_list_header" + override val contentType = RelayListItemContentType.CUSTOM_LIST_HEADER + } + data class CustomListItem( - override val item: RelayItem.CustomList, + override val hop: Hop.Single<RelayItem.CustomList>, override val isSelected: Boolean = false, override val expanded: Boolean = false, override val state: RelayListItemState? = null, override val itemPosition: ItemPosition = ItemPosition.Single, ) : SelectableItem { + val item = hop.relay override val key = item.id override val depth: Int = 0 override val contentType = RelayListItemContentType.CUSTOM_LIST_ITEM + override val canExpand: Boolean = item.hasChildren } data class CustomListEntryItem( val parentId: CustomListId, val parentName: CustomListName, - override val item: RelayItem.Location, + override val hop: Hop.Single<RelayItem.Location>, override val expanded: Boolean, override val depth: Int = 0, override val state: RelayListItemState? = null, override val itemPosition: ItemPosition, ) : SelectableItem { + val item = hop.relay override val key = parentId to item.id // Can't be displayed as selected override val isSelected: Boolean = false override val contentType = RelayListItemContentType.CUSTOM_LIST_ENTRY_ITEM + override val canExpand: Boolean = item.hasChildren } data class CustomListFooter(val hasCustomList: Boolean) : RelayListItem { @@ -77,15 +89,40 @@ sealed interface RelayListItem { } data class GeoLocationItem( - override val item: RelayItem.Location, + override val hop: Hop.Single<RelayItem.Location>, override val isSelected: Boolean = false, override val depth: Int = 0, override val expanded: Boolean = false, override val state: RelayListItemState? = null, override val itemPosition: ItemPosition, ) : SelectableItem { + val item = hop.relay override val key = item.id override val contentType = RelayListItemContentType.LOCATION_ITEM + override val canExpand: Boolean = item.hasChildren + } + + data object RecentsListHeader : RelayListItem { + override val key = "recents_list_header" + override val contentType = RelayListItemContentType.RECENT_LIST_HEADER + } + + data class RecentListItem( + override val hop: Hop, + override val isSelected: Boolean = false, + override val expanded: Boolean = false, + override val state: RelayListItemState? = null, + override val itemPosition: ItemPosition = ItemPosition.Single, + ) : SelectableItem { + override val key = "recents$hop" + override val depth: Int = 0 + override val contentType = RelayListItemContentType.RECENT_LIST_ITEM + override val canExpand: Boolean = false + } + + data object RecentsListFooter : RelayListItem { + override val key = "recents_list_footer" + override val contentType = RelayListItemContentType.RECENT_LIST_FOOTER } data class LocationsEmptyText(val searchTerm: String) : RelayListItem { @@ -97,6 +134,11 @@ sealed interface RelayListItem { override val key = "empty_relay_list" override val contentType = RelayListItemContentType.EMPTY_RELAY_LIST } + + class SectionDivider : RelayListItem { + override val key: String = "section_divider_${this.hashCode()}" + override val contentType = RelayListItemContentType.SECTION_DIVIDER + } } data class CheckableRelayListItem( @@ -130,3 +172,9 @@ sealed interface ItemPosition { else -> false } } + +fun Hop.displayName(context: Context): String = + when (this) { + is Hop.Multi -> context.getString(R.string.x_via_x, exit.name, entry.name) + is Hop.Single<*> -> relay.name + } diff --git a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayListItemPreviewData.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayListItemPreviewData.kt index 5776601168..58ae2f2e82 100644 --- a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayListItemPreviewData.kt +++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayListItemPreviewData.kt @@ -3,6 +3,7 @@ package net.mullvad.mullvadvpn.lib.ui.component.relaylist import net.mullvad.mullvadvpn.lib.model.CustomList import net.mullvad.mullvadvpn.lib.model.CustomListId import net.mullvad.mullvadvpn.lib.model.CustomListName +import net.mullvad.mullvadvpn.lib.model.Hop import net.mullvad.mullvadvpn.lib.model.RelayItem object RelayListItemPreviewData { @@ -16,23 +17,25 @@ object RelayListItemPreviewData { // Add custom list items if (includeCustomLists) { RelayListItem.CustomListItem( - item = - RelayItem.CustomList( - customList = - CustomList( - id = CustomListId("custom_list_id"), - name = CustomListName.fromString("Custom List"), - locations = emptyList(), - ), - locations = - listOf( - generateRelayItemCountry( - name = "Country", - cityNames = listOf("City"), - relaysPerCity = 2, - active = true, - ) - ), + hop = + Hop.Single( + RelayItem.CustomList( + customList = + CustomList( + id = CustomListId("custom_list_id"), + name = CustomListName.fromString("Custom List"), + locations = emptyList(), + ), + locations = + listOf( + generateRelayItemCountry( + name = "Country", + cityNames = listOf("City"), + relaysPerCity = 2, + active = true, + ) + ), + ) ), isSelected = false, state = null, @@ -63,7 +66,7 @@ object RelayListItemPreviewData { addAll( listOf( RelayListItem.GeoLocationItem( - item = locations[0], + hop = Hop.Single(locations[0]), isSelected = false, depth = 0, expanded = true, @@ -71,7 +74,7 @@ object RelayListItemPreviewData { itemPosition = ItemPosition.Middle, ), RelayListItem.GeoLocationItem( - item = locations[0].cities[0], + hop = Hop.Single(locations[0].cities[0]), isSelected = true, depth = 1, expanded = false, @@ -79,7 +82,7 @@ object RelayListItemPreviewData { itemPosition = ItemPosition.Middle, ), RelayListItem.GeoLocationItem( - item = locations[0].cities[1], + hop = Hop.Single(locations[0].cities[1]), isSelected = false, depth = 1, expanded = true, @@ -87,7 +90,7 @@ object RelayListItemPreviewData { itemPosition = ItemPosition.Middle, ), RelayListItem.GeoLocationItem( - item = locations[0].cities[1].relays[0], + hop = Hop.Single(locations[0].cities[1].relays[0]), isSelected = false, depth = 2, expanded = false, @@ -95,7 +98,7 @@ object RelayListItemPreviewData { itemPosition = ItemPosition.Middle, ), RelayListItem.GeoLocationItem( - item = locations[0].cities[1].relays[1], + hop = Hop.Single(locations[0].cities[1].relays[1]), isSelected = false, depth = 2, expanded = false, @@ -103,7 +106,7 @@ object RelayListItemPreviewData { itemPosition = ItemPosition.Middle, ), RelayListItem.GeoLocationItem( - item = locations[1], + hop = Hop.Single(locations[1]), isSelected = false, depth = 0, expanded = false, diff --git a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItem.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItem.kt index 83b24ff137..289eb5aa9f 100644 --- a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItem.kt +++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItem.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -72,7 +73,7 @@ fun SelectableRelayListItem( modifier = modifier, shape = relayListItem.itemPosition.toShape(), selected = relayListItem.isSelected, - enabled = relayListItem.item.active, + enabled = relayListItem.hop.isActive, content = { Row( modifier = @@ -84,7 +85,7 @@ fun SelectableRelayListItem( ) { val iconTint = when { - !relayListItem.item.active -> MaterialTheme.colorScheme.error + !relayListItem.hop.isActive -> MaterialTheme.colorScheme.error relayListItem.isSelected -> MaterialTheme.colorScheme.tertiary else -> Color.Transparent } @@ -94,14 +95,14 @@ fun SelectableRelayListItem( contentDescription = null, tint = iconTint, ) - } else if (!relayListItem.item.active) { + } else if (!relayListItem.hop.isActive) { InactiveRelayIndicator(iconTint) } Name( - name = relayListItem.item.name, + name = relayListItem.hop.displayName(LocalContext.current), state = relayListItem.state, - active = relayListItem.item.active, + active = relayListItem.hop.isActive, ) } }, @@ -111,7 +112,7 @@ fun SelectableRelayListItem( else ({}), onLongClick = onLongClick, trailingContent = - if (relayListItem.item.hasChildren) { + if (relayListItem.canExpand) { { ExpandChevron( isExpanded = relayListItem.expanded, diff --git a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItemPreviewParameterProvider.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItemPreviewParameterProvider.kt index 732c03bbc4..812f4de60e 100644 --- a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItemPreviewParameterProvider.kt +++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItemPreviewParameterProvider.kt @@ -1,6 +1,7 @@ package net.mullvad.mullvadvpn.lib.ui.component.relaylist import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import net.mullvad.mullvadvpn.lib.model.Hop class SelectableRelayListItemPreviewParameterProvider : PreviewParameterProvider<List<RelayListItem.SelectableItem>> { @@ -8,55 +9,65 @@ class SelectableRelayListItemPreviewParameterProvider : sequenceOf( listOf( RelayListItem.GeoLocationItem( - item = - generateRelayItemCountry( - name = "Relay country Active", - cityNames = listOf("Relay city 1", "Relay city 2"), - relaysPerCity = 2, + hop = + Hop.Single( + generateRelayItemCountry( + name = "Relay country Active", + cityNames = listOf("Relay city 1", "Relay city 2"), + relaysPerCity = 2, + ) ), isSelected = true, expanded = false, itemPosition = ItemPosition.Single, ), RelayListItem.GeoLocationItem( - item = - generateRelayItemCountry( - name = "Not Enabled Relay country", - cityNames = listOf("Not Enabled city"), - relaysPerCity = 1, - active = false, + hop = + Hop.Single( + generateRelayItemCountry( + name = "Not Enabled Relay country", + cityNames = listOf("Not Enabled city"), + relaysPerCity = 1, + active = false, + ) ), isSelected = false, itemPosition = ItemPosition.Single, ), RelayListItem.GeoLocationItem( - item = - generateRelayItemCountry( - name = "Relay country Expanded", - cityNames = listOf("Normal city"), - relaysPerCity = 2, + hop = + Hop.Single( + generateRelayItemCountry( + name = "Relay country Expanded", + cityNames = listOf("Normal city"), + relaysPerCity = 2, + ) ), isSelected = true, expanded = true, itemPosition = ItemPosition.Single, ), RelayListItem.GeoLocationItem( - item = - generateRelayItemCountry( - name = "Country and city Expanded", - cityNames = listOf("Expanded city A", "Expanded city B"), - relaysPerCity = 2, + hop = + Hop.Single( + generateRelayItemCountry( + name = "Country and city Expanded", + cityNames = listOf("Expanded city A", "Expanded city B"), + relaysPerCity = 2, + ) ), isSelected = false, itemPosition = ItemPosition.Single, ), RelayListItem.GeoLocationItem( - item = - generateRelayItemCountry( - name = "Country selected but inactive", - cityNames = listOf("Expanded city A", "Expanded city B"), - relaysPerCity = 2, - active = false, + hop = + Hop.Single( + generateRelayItemCountry( + name = "Country selected but inactive", + cityNames = listOf("Expanded city A", "Expanded city B"), + relaysPerCity = 2, + active = false, + ) ), isSelected = true, itemPosition = ItemPosition.Single, |
