summaryrefslogtreecommitdiffhomepage
path: root/android/lib
diff options
context:
space:
mode:
authorKalle Lindström <karl.lindstrom@mullvad.net>2025-07-22 14:26:22 +0200
committerKalle Lindström <karl.lindstrom@mullvad.net>2025-07-22 14:26:22 +0200
commitb2fc803af349205bc40d7cd00e0a480536c3d09e (patch)
treed603241a7e9ed6284f89704140f02c1a828518cb /android/lib
parent75501a665b1bb7257cacd79f1eca84c839929725 (diff)
parent526ecbf7d85c8abe7af08daf04dc4bc0c6df109c (diff)
downloadmullvadvpn-b2fc803af349205bc40d7cd00e0a480536c3d09e.tar.xz
mullvadvpn-b2fc803af349205bc40d7cd00e0a480536c3d09e.zip
Merge branch 'implement-recents-support-ui'
Diffstat (limited to 'android/lib')
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt27
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt25
-rw-r--r--android/lib/resource/src/main/res/values/strings.xml6
-rw-r--r--android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayListItem.kt66
-rw-r--r--android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayListItemPreviewData.kt49
-rw-r--r--android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItem.kt13
-rw-r--r--android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItemPreviewParameterProvider.kt65
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,