Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/src/main/java/com/tailscale/ipn/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ open class UninitializedApp : Application() {
}

fun allowSelectedPackages(): Boolean {
return getUnencryptedPrefs().getBoolean(ALLOW_SELECTED_APPS_KEY, false)
return getUnencryptedPrefs().getBoolean(ALLOW_SELECTED_APPS_KEY, true)
}

fun getAppScopedViewModel(): AppViewModel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,25 @@
package com.tailscale.ipn.ui.view

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
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.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
Expand All @@ -43,6 +42,7 @@ import com.tailscale.ipn.ui.util.Lists
import com.tailscale.ipn.ui.util.set
import com.tailscale.ipn.ui.viewModel.SplitTunnelAppPickerViewModel

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SplitTunnelAppPickerView(
backToSettings: BackNavigation,
Expand All @@ -54,7 +54,6 @@ fun SplitTunnelAppPickerView(
val builtInDisallowedPackageNames: List<String> = App.get().builtInDisallowedPackageNames
val mdmIncludedPackages by model.mdmIncludedPackages.collectAsState()
val mdmExcludedPackages by model.mdmExcludedPackages.collectAsState()
val showHeaderMenu by model.showHeaderMenu.collectAsState()
val showSwitchDialog by model.showSwitchDialog.collectAsState()

if (showSwitchDialog) {
Expand All @@ -70,18 +69,7 @@ fun SplitTunnelAppPickerView(

Scaffold(
topBar = {
Header(
titleRes = R.string.split_tunneling,
onBack = backToSettings,
actions = {
Row {
FusMenu(viewModel = model, onSwitchClick = { model.showSwitchDialog.set(true) })
IconButton(onClick = { model.showHeaderMenu.set(!showHeaderMenu) }) {
Icon(Icons.Default.MoreVert, "menu")
}
}
},
)
Header(titleRes = R.string.split_tunneling, onBack = backToSettings)
},
) { innerPadding ->
LazyColumn(modifier = Modifier.padding(innerPadding)) {
Expand Down Expand Up @@ -111,13 +99,57 @@ fun SplitTunnelAppPickerView(
.selected_apps_will_access_the_internet_directly_without_using_tailscale))
})
}
item("modeSelector") {
SingleChoiceSegmentedButtonRow(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
) {
SegmentedButton(
selected = !allowSelected,
onClick = {
if (allowSelected) {
model.showSwitchDialog.set(true)
}
},
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2),
) {
Text(stringResource(R.string.split_tunnel_mode_exclude))
}
SegmentedButton(
selected = allowSelected,
onClick = {
if (!allowSelected) {
model.showSwitchDialog.set(true)
}
},
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2),
) {
Text(stringResource(R.string.split_tunnel_mode_include))
}
}
}
item("resolversHeader") {
Lists.SectionDivider(
stringResource(
if (allowSelected) R.string.count_included_apps else R.string.count_excluded_apps,
selectedPackageNames.count(),
))
}
item("bulkActions") {
Row(modifier = Modifier.padding(horizontal = 8.dp)) {
TextButton(
onClick = { model.selectAll() },
enabled = installedApps.isNotEmpty(),
) {
Text(stringResource(R.string.select_all_apps))
}
TextButton(
onClick = { model.deselectAll() },
enabled = installedApps.isNotEmpty(),
) {
Text(stringResource(R.string.deselect_all_apps))
}
}
}
if (installedApps.isEmpty()) {
item("spinner") {
Box(
Expand Down Expand Up @@ -177,30 +209,7 @@ fun SplitTunnelAppPickerView(
}

@Composable
fun FusMenu(viewModel: SplitTunnelAppPickerViewModel, onSwitchClick: (() -> Unit)) {
val expanded by viewModel.showHeaderMenu.collectAsState()
val allowSelected by viewModel.allowSelected.collectAsState()

DropdownMenu(
expanded = expanded,
onDismissRequest = { viewModel.showHeaderMenu.set(false) },
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainer),
) {
MenuItem(
onClick = {
viewModel.showHeaderMenu.set(false)
onSwitchClick()
},
text =
stringResource(
if (allowSelected) R.string.switch_to_select_to_exclude
else R.string.switch_to_select_to_include),
)
}
}

@Composable
fun SwitchAlertDialog(allowSelected: Boolean, onConfirm: (() -> Unit), onDismiss: (() -> Unit)) {
fun SwitchAlertDialog(allowSelected: Boolean, onConfirm: () -> Unit, onDismiss: () -> Unit) {
val switchString =
stringResource(
if (allowSelected) R.string.switch_to_select_to_exclude
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ class SplitTunnelAppPickerViewModel : ViewModel() {
val selectedPackageNames: StateFlow<List<String>> = MutableStateFlow(listOf())

val allowSelected: StateFlow<Boolean> = MutableStateFlow(App.get().allowSelectedPackages())
val showHeaderMenu: StateFlow<Boolean> = MutableStateFlow(false)
val showSwitchDialog: StateFlow<Boolean> = MutableStateFlow(false)

val mdmExcludedPackages: StateFlow<SettingState<String?>> = MDMSettings.excludedPackages.flow
Expand Down Expand Up @@ -80,6 +79,27 @@ class SplitTunnelAppPickerViewModel : ViewModel() {
debounceSave()
}

private fun toggleablePackageNames(): List<String> =
installedApps.value
.map { it.packageName }
.filter { !App.get().builtInDisallowedPackageNames.contains(it) }

fun selectAll() {
val newSet = selectedPackageNames.value.toMutableSet()
toggleablePackageNames().forEach { newSet.add(it) }
selectedPackageNames.set(
installedApps.value.map { it.packageName }.filter { newSet.contains(it) })
debounceSave()
}

fun deselectAll() {
val builtIn = App.get().builtInDisallowedPackageNames.toSet()
val kept = selectedPackageNames.value.filter { builtIn.contains(it) }.toSet()
selectedPackageNames.set(
installedApps.value.map { it.packageName }.filter { kept.contains(it) })
debounceSave()
}

private fun debounceSave() {
saveJob?.cancel()
saveJob =
Expand Down
4 changes: 4 additions & 0 deletions android/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,10 @@
<string name="selected_apps_will_access_tailscale">Only apps you select will be allowed to access Tailscale.</string>
<string name="count_excluded_apps">Excluded apps (%1$s)</string>
<string name="count_included_apps">Included apps (%1$s)</string>
<string name="split_tunnel_mode_exclude">Exclude</string>
<string name="split_tunnel_mode_include">Include only</string>
<string name="select_all_apps">Select all</string>
<string name="deselect_all_apps">Deselect all</string>
<string name="certain_apps_are_not_routed_via_tailscale">Certain apps are not routed via Tailscale on this device. This setting is managed by your organization and cannot be changed by you. For more information, contact your network administrator.</string>
<string name="only_specific_apps_are_routed_via_tailscale">Only specific apps are routed via Tailscale on this device. This setting is managed by your organization and cannot be changed by you. For more information, contact your network administrator.</string>
<string name="switch_to_select_to_include">Switch to including</string>
Expand Down