summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorMaryamShaghaghi <122574719+MaryamShaghaghi@users.noreply.github.com>2023-12-06 10:18:39 +0100
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2024-02-15 23:28:31 +0100
commit6aff63d1b8c0ce415ac1222fe36e19101d0e49ec (patch)
tree044010e935078aaf995bae0843bb626b95f17c6d /android
parentbf4fc6d570e6ef468c040dadeaabb1b943124f41 (diff)
downloadmullvadvpn-6aff63d1b8c0ce415ac1222fe36e19101d0e49ec.tar.xz
mullvadvpn-6aff63d1b8c0ce415ac1222fe36e19101d0e49ec.zip
Add toggle button for split tunneling
Co-Authored-By: Boban Sijuk <49131853+Boki91@users.noreply.github.com>
Diffstat (limited to 'android')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/SplitTunnelingCell.kt18
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt308
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SplitTunnelingUiState.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SplitTunneling.kt14
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt26
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt12
-rw-r--r--android/lib/resource/src/main/res/values/strings.xml1
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SplitTunneling.kt9
8 files changed, 264 insertions, 129 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/SplitTunnelingCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/SplitTunnelingCell.kt
index 6ad5675a43..25b6f71445 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/SplitTunnelingCell.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/SplitTunnelingCell.kt
@@ -41,8 +41,18 @@ private fun PreviewTunnelingCell() {
modifier =
Modifier.background(color = MaterialTheme.colorScheme.background).padding(20.dp)
) {
- SplitTunnelingCell(title = "Mullvad VPN", packageName = "", isSelected = false)
- SplitTunnelingCell(title = "Mullvad VPN", packageName = "", isSelected = true)
+ SplitTunnelingCell(
+ title = "Mullvad VPN",
+ packageName = "",
+ isSelected = false,
+ enabled = true
+ )
+ SplitTunnelingCell(
+ title = "Mullvad VPN",
+ packageName = "",
+ isSelected = true,
+ enabled = true
+ )
}
}
}
@@ -52,6 +62,7 @@ fun SplitTunnelingCell(
title: String,
packageName: String?,
isSelected: Boolean,
+ enabled: Boolean,
modifier: Modifier = Modifier,
backgroundColor: Color =
MaterialTheme.colorScheme.primary
@@ -110,6 +121,7 @@ fun SplitTunnelingCell(
},
onCellClicked = onCellClicked,
background = backgroundColor,
- modifier = modifier
+ modifier = modifier,
+ isRowEnabled = enabled
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt
index aae3f8274e..12d7a62d78 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt
@@ -2,22 +2,22 @@ package net.mullvad.mullvadvpn.compose.screen
import android.graphics.Bitmap
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
@@ -29,9 +29,11 @@ import net.mullvad.mullvadvpn.applist.AppData
import net.mullvad.mullvadvpn.compose.cell.HeaderCell
import net.mullvad.mullvadvpn.compose.cell.HeaderSwitchComposeCell
import net.mullvad.mullvadvpn.compose.cell.SplitTunnelingCell
+import net.mullvad.mullvadvpn.compose.cell.SwitchComposeSubtitleCell
import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge
import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar
+import net.mullvad.mullvadvpn.compose.component.textResource
import net.mullvad.mullvadvpn.compose.constant.CommonContentKey
import net.mullvad.mullvadvpn.compose.constant.ContentType
import net.mullvad.mullvadvpn.compose.constant.SplitTunnelingContentKey
@@ -41,6 +43,8 @@ import net.mullvad.mullvadvpn.compose.state.SplitTunnelingUiState
import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
+import net.mullvad.mullvadvpn.lib.theme.color.AlphaDisabled
+import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
import net.mullvad.mullvadvpn.util.getApplicationIconBitmapOrNull
import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel
import org.koin.androidx.compose.koinViewModel
@@ -52,17 +56,18 @@ private fun PreviewSplitTunnelingScreen() {
SplitTunnelingScreen(
uiState =
SplitTunnelingUiState.ShowAppList(
+ enabled = true,
excludedApps =
listOf(
AppData(
packageName = "my.package.a",
name = "TitleA",
- iconRes = R.drawable.icon_alert,
+ iconRes = R.drawable.icon_alert
),
AppData(
packageName = "my.package.b",
name = "TitleB",
- iconRes = R.drawable.icon_chevron,
+ iconRes = R.drawable.icon_chevron
)
),
includedApps =
@@ -88,6 +93,7 @@ fun SplitTunneling(navigator: DestinationsNavigator) {
val packageManager = remember(context) { context.packageManager }
SplitTunnelingScreen(
uiState = state,
+ onEnableSplitTunneling = viewModel::onEnableSplitTunneling,
onShowSystemAppsClick = viewModel::onShowSystemAppsClick,
onExcludeAppClick = viewModel::onExcludeAppClick,
onIncludeAppClick = viewModel::onIncludeAppClick,
@@ -99,14 +105,14 @@ fun SplitTunneling(navigator: DestinationsNavigator) {
}
@Composable
-@OptIn(ExperimentalFoundationApi::class)
fun SplitTunnelingScreen(
- uiState: SplitTunnelingUiState = SplitTunnelingUiState.Loading,
+ uiState: SplitTunnelingUiState,
+ onEnableSplitTunneling: (Boolean) -> Unit = {},
onShowSystemAppsClick: (show: Boolean) -> Unit = {},
onExcludeAppClick: (packageName: String) -> Unit = {},
onIncludeAppClick: (packageName: String) -> Unit = {},
onBackClick: () -> Unit = {},
- onResolveIcon: (String) -> Bitmap? = { null },
+ onResolveIcon: (String) -> Bitmap? = { null }
) {
val focusManager = LocalFocusManager.current
@@ -120,115 +126,201 @@ fun SplitTunnelingScreen(
horizontalAlignment = Alignment.CenterHorizontally,
state = lazyListState
) {
- item(key = CommonContentKey.DESCRIPTION, contentType = ContentType.DESCRIPTION) {
- Box(modifier = Modifier.fillMaxWidth()) {
- Text(
- style = MaterialTheme.typography.labelMedium,
- text = stringResource(id = R.string.split_tunneling_description),
- modifier =
- Modifier.padding(
- start = Dimens.mediumPadding,
- end = Dimens.mediumPadding,
- bottom = Dimens.mediumPadding
- )
+ enabledToggle(
+ enabled = uiState.enabled,
+ onEnableSplitTunneling = onEnableSplitTunneling
+ )
+ description(enabled = uiState.enabled)
+ spacer()
+ when (uiState) {
+ is SplitTunnelingUiState.Loading -> {
+ loading()
+ }
+ is SplitTunnelingUiState.ShowAppList -> {
+ appList(
+ uiState = uiState,
+ focusManager = focusManager,
+ onShowSystemAppsClick = onShowSystemAppsClick,
+ onExcludeAppClick = onExcludeAppClick,
+ onIncludeAppClick = onIncludeAppClick,
+ onResolveIcon = onResolveIcon
)
}
}
- when (uiState) {
- SplitTunnelingUiState.Loading -> {
- item(key = CommonContentKey.PROGRESS, contentType = ContentType.PROGRESS) {
- MullvadCircularProgressIndicatorLarge()
- }
+ }
+ }
+}
+
+private fun LazyListScope.enabledToggle(
+ enabled: Boolean,
+ onEnableSplitTunneling: (Boolean) -> Unit
+) {
+ item {
+ HeaderSwitchComposeCell(
+ title = textResource(id = R.string.enable),
+ isToggled = enabled,
+ onCellClicked = onEnableSplitTunneling
+ )
+ }
+}
+
+private fun LazyListScope.description(enabled: Boolean) {
+ item(key = CommonContentKey.DESCRIPTION, contentType = ContentType.DESCRIPTION) {
+ SwitchComposeSubtitleCell(
+ text =
+ if (enabled) {
+ stringResource(id = R.string.split_tunneling_description)
+ } else {
+ stringResource(id = R.string.split_tunneling_disabled_description)
}
- is SplitTunnelingUiState.ShowAppList -> {
- if (uiState.excludedApps.isNotEmpty()) {
- itemWithDivider(
- key = SplitTunnelingContentKey.EXCLUDED_APPLICATIONS,
- contentType = ContentType.HEADER
- ) {
- HeaderCell(
- modifier = Modifier.animateItemPlacement(),
- text = stringResource(id = R.string.exclude_applications),
- background = MaterialTheme.colorScheme.primary,
- )
- }
- itemsIndexedWithDivider(
- items = uiState.excludedApps,
- key = { _, listItem -> listItem.packageName },
- contentType = { _, _ -> ContentType.ITEM }
- ) { index, listItem ->
- SplitTunnelingCell(
- title = listItem.name,
- packageName = listItem.packageName,
- isSelected = true,
- modifier = Modifier.animateItemPlacement().fillMaxWidth(),
- onResolveIcon = onResolveIcon
- ) {
- // Move focus down unless the clicked item was the last in this
- // section.
- if (index < uiState.excludedApps.size - 1) {
- focusManager.moveFocus(FocusDirection.Down)
- } else {
- focusManager.moveFocus(FocusDirection.Up)
- }
+ )
+ }
+}
- onIncludeAppClick(listItem.packageName)
- }
- }
- item(key = CommonContentKey.SPACER, contentType = ContentType.SPACER) {
- Spacer(
- modifier =
- Modifier.animateItemPlacement().height(Dimens.mediumPadding)
- )
- }
- }
+private fun LazyListScope.loading() {
+ item(key = CommonContentKey.PROGRESS, contentType = ContentType.PROGRESS) {
+ MullvadCircularProgressIndicatorLarge()
+ }
+}
- itemWithDivider(
- key = SplitTunnelingContentKey.SHOW_SYSTEM_APPLICATIONS,
- contentType = ContentType.OTHER_ITEM
- ) {
- HeaderSwitchComposeCell(
- title = stringResource(id = R.string.show_system_apps),
- isToggled = uiState.showSystemApps,
- onCellClicked = { newValue -> onShowSystemAppsClick(newValue) },
- modifier = Modifier.animateItemPlacement()
- )
- }
- itemWithDivider(
- key = SplitTunnelingContentKey.INCLUDED_APPLICATIONS,
- contentType = ContentType.HEADER
- ) {
- HeaderCell(
- modifier = Modifier.animateItemPlacement(),
- text = stringResource(id = R.string.all_applications),
- background = MaterialTheme.colorScheme.primary,
- )
- }
- itemsIndexedWithDivider(
- items = uiState.includedApps,
- key = { _, listItem -> listItem.packageName },
- contentType = { _, _ -> ContentType.ITEM }
- ) { index, listItem ->
- SplitTunnelingCell(
- title = listItem.name,
- packageName = listItem.packageName,
- isSelected = false,
- modifier = Modifier.animateItemPlacement().fillMaxWidth(),
- onResolveIcon = onResolveIcon
- ) {
- // Move focus down unless the clicked item was the last in this
- // section.
- if (index < uiState.includedApps.size - 1) {
- focusManager.moveFocus(FocusDirection.Down)
- } else {
- focusManager.moveFocus(FocusDirection.Up)
- }
+private fun LazyListScope.appList(
+ uiState: SplitTunnelingUiState.ShowAppList,
+ focusManager: FocusManager,
+ onShowSystemAppsClick: (show: Boolean) -> Unit,
+ onExcludeAppClick: (packageName: String) -> Unit,
+ onIncludeAppClick: (packageName: String) -> Unit,
+ onResolveIcon: (String) -> Bitmap?
+) {
+ if (uiState.excludedApps.isNotEmpty()) {
+ headerItem(
+ key = SplitTunnelingContentKey.EXCLUDED_APPLICATIONS,
+ textId = R.string.exclude_applications,
+ enabled = uiState.enabled
+ )
+ appItems(
+ apps = uiState.excludedApps,
+ focusManager = focusManager,
+ onAppClick = onIncludeAppClick,
+ onResolveIcon = onResolveIcon,
+ enabled = uiState.enabled,
+ excluded = true
+ )
+ spacer()
+ }
+ systemAppsToggle(
+ showSystemApps = uiState.showSystemApps,
+ onShowSystemAppsClick = onShowSystemAppsClick,
+ enabled = uiState.enabled
+ )
+ headerItem(
+ key = SplitTunnelingContentKey.INCLUDED_APPLICATIONS,
+ textId = R.string.all_applications,
+ enabled = uiState.enabled
+ )
+ appItems(
+ apps = uiState.includedApps,
+ focusManager = focusManager,
+ onAppClick = onExcludeAppClick,
+ onResolveIcon = onResolveIcon,
+ enabled = uiState.enabled,
+ excluded = false
+ )
+}
- onExcludeAppClick(listItem.packageName)
+@OptIn(ExperimentalFoundationApi::class)
+private fun LazyListScope.appItems(
+ apps: List<AppData>,
+ focusManager: FocusManager,
+ onAppClick: (String) -> Unit,
+ onResolveIcon: (String) -> Bitmap?,
+ enabled: Boolean,
+ excluded: Boolean
+) {
+ itemsIndexedWithDivider(
+ items = apps,
+ key = { _, listItem -> listItem.packageName },
+ contentType = { _, _ -> ContentType.ITEM }
+ ) { index, listItem ->
+ SplitTunnelingCell(
+ title = listItem.name,
+ packageName = listItem.packageName,
+ isSelected = excluded,
+ enabled = enabled,
+ modifier =
+ Modifier.animateItemPlacement()
+ .fillMaxWidth()
+ .alpha(
+ if (enabled) {
+ AlphaVisible
+ } else {
+ AlphaDisabled
}
- }
- }
+ ),
+ onResolveIcon = onResolveIcon
+ ) {
+ // Move focus down unless the clicked item was the last in this
+ // section.
+ if (index < apps.size - 1) {
+ focusManager.moveFocus(FocusDirection.Down)
+ } else {
+ focusManager.moveFocus(FocusDirection.Up)
}
+
+ onAppClick(listItem.packageName)
}
}
}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun LazyListScope.headerItem(key: String, textId: Int, enabled: Boolean) {
+ itemWithDivider(key = key, contentType = ContentType.HEADER) {
+ HeaderCell(
+ modifier =
+ Modifier.animateItemPlacement()
+ .alpha(
+ if (enabled) {
+ AlphaVisible
+ } else {
+ AlphaDisabled
+ }
+ ),
+ text = stringResource(id = textId),
+ background = MaterialTheme.colorScheme.primary
+ )
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun LazyListScope.systemAppsToggle(
+ showSystemApps: Boolean,
+ onShowSystemAppsClick: (show: Boolean) -> Unit,
+ enabled: Boolean
+) {
+ itemWithDivider(
+ key = SplitTunnelingContentKey.SHOW_SYSTEM_APPLICATIONS,
+ contentType = ContentType.OTHER_ITEM
+ ) {
+ HeaderSwitchComposeCell(
+ title = stringResource(id = R.string.show_system_apps),
+ isToggled = showSystemApps,
+ onCellClicked = { newValue -> onShowSystemAppsClick(newValue) },
+ isEnabled = enabled,
+ modifier =
+ Modifier.animateItemPlacement()
+ .alpha(
+ if (enabled) {
+ AlphaVisible
+ } else {
+ AlphaDisabled
+ }
+ )
+ )
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun LazyListScope.spacer() {
+ item(contentType = ContentType.SPACER) {
+ Spacer(modifier = Modifier.animateItemPlacement().height(Dimens.mediumPadding))
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SplitTunnelingUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SplitTunnelingUiState.kt
index 7752293516..24552444e9 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SplitTunnelingUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SplitTunnelingUiState.kt
@@ -3,9 +3,12 @@ package net.mullvad.mullvadvpn.compose.state
import net.mullvad.mullvadvpn.applist.AppData
sealed interface SplitTunnelingUiState {
- data object Loading : SplitTunnelingUiState
+ val enabled: Boolean
+
+ data class Loading(override val enabled: Boolean = false) : SplitTunnelingUiState
data class ShowAppList(
+ override val enabled: Boolean = false,
val excludedApps: List<AppData> = emptyList(),
val includedApps: List<AppData> = emptyList(),
val showSystemApps: Boolean = false
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SplitTunneling.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SplitTunneling.kt
index 823d222443..666d772184 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SplitTunneling.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SplitTunneling.kt
@@ -10,11 +10,12 @@ class SplitTunneling(private val connection: Messenger, eventDispatcher: EventDi
private var _excludedApps by
observable(emptySet<String>()) { _, _, apps -> excludedAppsChange.invoke(apps) }
- var enabled by
- observable(false) { _, wasEnabled, isEnabled ->
- if (wasEnabled != isEnabled) {
- connection.send(Request.SetEnableSplitTunneling(isEnabled).message)
- }
+ var enabled by observable(false) { _, _, isEnabled -> enabledChange.invoke(isEnabled) }
+
+ var enabledChange: (enabled: Boolean) -> Unit = {}
+ set(value) {
+ field = value
+ synchronized(this) { value.invoke(enabled) }
}
var excludedAppsChange: (apps: Set<String>) -> Unit = {}
@@ -41,4 +42,7 @@ class SplitTunneling(private val connection: Messenger, eventDispatcher: EventDi
connection.send(Request.IncludeApp(appPackageName).message)
fun persist() = connection.send(Request.PersistExcludedApps.message)
+
+ fun enableSplitTunneling(isEnabled: Boolean) =
+ connection.send(Request.SetEnableSplitTunneling(isEnabled).message)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt
index bb543d85cf..833117c046 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt
@@ -50,11 +50,13 @@ class SplitTunnelingViewModel(
.flatMapLatest { serviceConnection ->
combine(
serviceConnection.splitTunneling.excludedAppsCallbackFlow(),
+ serviceConnection.splitTunneling.enabledCallbackFlow(),
allApps,
- showSystemApps
- ) { excludedApps, allApps, showSystemApps ->
+ showSystemApps,
+ ) { excludedApps, enabled, allApps, showSystemApps ->
SplitTunnelingViewModelState(
excludedApps = excludedApps,
+ enabled = enabled,
allApps = allApps,
showSystemApps = showSystemApps
)
@@ -72,16 +74,11 @@ class SplitTunnelingViewModel(
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(),
- SplitTunnelingUiState.Loading
+ SplitTunnelingUiState.Loading(enabled = false)
)
init {
- viewModelScope.launch(dispatcher) {
- if (serviceConnectionManager.splitTunneling()?.enabled == false) {
- serviceConnectionManager.splitTunneling()?.enabled = true
- }
- fetchApps()
- }
+ viewModelScope.launch(dispatcher) { fetchApps() }
}
override fun onCleared() {
@@ -89,6 +86,12 @@ class SplitTunnelingViewModel(
super.onCleared()
}
+ fun onEnableSplitTunneling(isEnabled: Boolean) {
+ viewModelScope.launch(dispatcher) {
+ serviceConnectionManager.splitTunneling()?.enableSplitTunneling(isEnabled)
+ }
+ }
+
fun onIncludeAppClick(packageName: String) {
viewModelScope.launch(dispatcher) {
serviceConnectionManager.splitTunneling()?.includeApp(packageName)
@@ -113,4 +116,9 @@ class SplitTunnelingViewModel(
excludedAppsChange = { apps -> trySend(apps) }
awaitClose { emptySet<String>() }
}
+
+ private fun SplitTunneling.enabledCallbackFlow() = callbackFlow {
+ enabledChange = { isEnabled -> trySend(isEnabled) }
+ awaitClose()
+ }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt
index 05bc6fb072..bc16662f00 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt
@@ -4,15 +4,23 @@ import net.mullvad.mullvadvpn.applist.AppData
import net.mullvad.mullvadvpn.compose.state.SplitTunnelingUiState
data class SplitTunnelingViewModelState(
+ val enabled: Boolean = false,
val excludedApps: Set<String> = emptySet(),
val allApps: List<AppData>? = null,
val showSystemApps: Boolean = false
) {
fun toUiState(): SplitTunnelingUiState {
return allApps
- ?.partition { appData -> excludedApps.contains(appData.packageName) }
+ ?.partition { appData ->
+ if (enabled) {
+ excludedApps.contains(appData.packageName)
+ } else {
+ false
+ }
+ }
?.let { (excluded, included) ->
SplitTunnelingUiState.ShowAppList(
+ enabled = enabled,
excludedApps = excluded.sortedBy { it.name },
includedApps =
if (showSystemApps) {
@@ -23,6 +31,6 @@ data class SplitTunnelingViewModelState(
.sortedBy { it.name },
showSystemApps = showSystemApps
)
- } ?: SplitTunnelingUiState.Loading
+ } ?: SplitTunnelingUiState.Loading(enabled = enabled)
}
}
diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml
index 836f72cadc..1c19dab826 100644
--- a/android/lib/resource/src/main/res/values/strings.xml
+++ b/android/lib/resource/src/main/res/values/strings.xml
@@ -259,4 +259,5 @@
<string name="loading_connecting">Connecting...</string>
<string name="loading_verifying">Verifying purchase...</string>
<string name="copied_logs_to_clipboard">Copied logs to clipboard</string>
+ <string name="split_tunneling_disabled_description">Split tunneling is disabled.</string>
</resources>
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SplitTunneling.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SplitTunneling.kt
index a683b1e4bf..4fbe89c82b 100644
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SplitTunneling.kt
+++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SplitTunneling.kt
@@ -17,7 +17,14 @@ class SplitTunneling(persistence: SplitTunnelingPersistence, endpoint: ServiceEn
}
}
- val onChange = EventNotifier<List<String>?>(excludedApps.toList())
+ val onChange =
+ EventNotifier(
+ if (enabled) {
+ excludedApps.toList()
+ } else {
+ null
+ }
+ )
init {
onChange.subscribe(this) { excludedApps ->