Skip to content

Commit 9c3378d

Browse files
authored
ui: add ability to advertise Android device as subnet router (#595)
1 parent a2850b1 commit 9c3378d

File tree

11 files changed

+721
-142
lines changed

11 files changed

+721
-142
lines changed

android/src/main/java/com/tailscale/ipn/MainActivity.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import com.tailscale.ipn.ui.view.RunExitNodeView
6969
import com.tailscale.ipn.ui.view.SearchView
7070
import com.tailscale.ipn.ui.view.SettingsView
7171
import com.tailscale.ipn.ui.view.SplitTunnelAppPickerView
72+
import com.tailscale.ipn.ui.view.SubnetRoutingView
7273
import com.tailscale.ipn.ui.view.TailnetLockSetupView
7374
import com.tailscale.ipn.ui.view.UserSwitcherNav
7475
import com.tailscale.ipn.ui.view.UserSwitcherView
@@ -185,6 +186,7 @@ class MainActivity : ComponentActivity() {
185186
onNavigateToDNSSettings = { navController.navigate("dnsSettings") },
186187
onNavigateToSplitTunneling = { navController.navigate("splitTunneling") },
187188
onNavigateToTailnetLock = { navController.navigate("tailnetLock") },
189+
onNavigateToSubnetRouting = { navController.navigate("subnetRouting")},
188190
onNavigateToMDMSettings = { navController.navigate("mdmSettings") },
189191
onNavigateToManagedBy = { navController.navigate("managedBy") },
190192
onNavigateToUserSwitcher = { navController.navigate("userSwitcher") },
@@ -247,6 +249,7 @@ class MainActivity : ComponentActivity() {
247249
composable("dnsSettings") { DNSSettingsView(backTo("settings")) }
248250
composable("splitTunneling") { SplitTunnelAppPickerView(backTo("settings")) }
249251
composable("tailnetLock") { TailnetLockSetupView(backTo("settings")) }
252+
composable("subnetRouting") { SubnetRoutingView(backTo("settings")) }
250253
composable("about") { AboutView(backTo("settings")) }
251254
composable("mdmSettings") { MDMSettingsDebugView(backTo("settings")) }
252255
composable("managedBy") { ManagedByView(backTo("settings")) }

android/src/main/java/com/tailscale/ipn/ui/Links.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ object Links {
2424
const val SUPPORT_URL = "https://tailscale.com/contact/support#support-form"
2525
const val TAILDROP_KB_URL = "https://tailscale.com/kb/1106/taildrop"
2626
const val TAILFS_KB_URL = "https://tailscale.com/kb/1106/taildrop"
27+
const val SUBNET_ROUTERS_KB_URL = "https://tailscale.com/kb/1019/subnets"
2728
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright (c) Tailscale Inc & AUTHORS
2+
// SPDX-License-Identifier: BSD-3-Clause
3+
4+
package com.tailscale.ipn.ui.view
5+
6+
import androidx.compose.foundation.layout.Column
7+
import androidx.compose.foundation.layout.Row
8+
import androidx.compose.foundation.layout.Spacer
9+
import androidx.compose.foundation.layout.height
10+
import androidx.compose.foundation.layout.padding
11+
import androidx.compose.foundation.layout.width
12+
import androidx.compose.material.icons.Icons
13+
import androidx.compose.material.icons.rounded.CheckCircle
14+
import androidx.compose.material.icons.rounded.Warning
15+
import androidx.compose.material3.Button
16+
import androidx.compose.material3.ButtonDefaults
17+
import androidx.compose.material3.Icon
18+
import androidx.compose.material3.MaterialTheme
19+
import androidx.compose.material3.Text
20+
import androidx.compose.material3.TextField
21+
import androidx.compose.runtime.Composable
22+
import androidx.compose.runtime.LaunchedEffect
23+
import androidx.compose.runtime.collectAsState
24+
import androidx.compose.runtime.getValue
25+
import androidx.compose.runtime.remember
26+
import androidx.compose.runtime.snapshotFlow
27+
import androidx.compose.ui.Alignment
28+
import androidx.compose.ui.Modifier
29+
import androidx.compose.ui.focus.FocusRequester
30+
import androidx.compose.ui.focus.focusRequester
31+
import androidx.compose.ui.platform.LocalWindowInfo
32+
import androidx.compose.ui.res.stringResource
33+
import androidx.compose.ui.unit.dp
34+
import com.tailscale.ipn.R
35+
import kotlinx.coroutines.flow.MutableStateFlow
36+
import kotlinx.coroutines.flow.StateFlow
37+
38+
/**
39+
* EditSubnetRouteDialogView is the content of the dialog that allows the user to add or edit a subnet route.
40+
*/
41+
@Composable
42+
fun EditSubnetRouteDialogView(
43+
valueFlow: MutableStateFlow<String>,
44+
isValueValidFlow: StateFlow<Boolean>,
45+
onValueChange: (String) -> Unit,
46+
onCommit: (String) -> Unit,
47+
onCancel: () -> Unit
48+
) {
49+
val value by valueFlow.collectAsState()
50+
val isValueValid by isValueValidFlow.collectAsState()
51+
val focusRequester = remember { FocusRequester() }
52+
53+
Column(
54+
modifier = Modifier.padding(16.dp),
55+
) {
56+
Text(text = stringResource(R.string.enter_valid_route))
57+
58+
Text(
59+
text = stringResource(R.string.route_help_text),
60+
color = MaterialTheme.colorScheme.secondary,
61+
fontSize = MaterialTheme.typography.bodySmall.fontSize
62+
)
63+
64+
Spacer(modifier = Modifier.height(8.dp))
65+
66+
TextField(
67+
value = value,
68+
onValueChange = { onValueChange(it) },
69+
singleLine = true,
70+
isError = !isValueValid,
71+
modifier = Modifier.focusRequester(focusRequester)
72+
)
73+
74+
Spacer(modifier = Modifier.height(8.dp))
75+
76+
Row(
77+
modifier = Modifier.align(Alignment.End)
78+
) {
79+
Button(colors = ButtonDefaults.outlinedButtonColors(), onClick = {
80+
onCancel()
81+
}) {
82+
Text(stringResource(R.string.cancel))
83+
}
84+
85+
Spacer(modifier = Modifier.width(8.dp))
86+
87+
Button(onClick = {
88+
onCommit(value)
89+
}, enabled = value.isNotEmpty() && isValueValid) {
90+
Text(stringResource(R.string.ok))
91+
}
92+
}
93+
}
94+
95+
// When the dialog is opened, focus on the text field to present the keyboard auto-magically.
96+
val windowInfo = LocalWindowInfo.current
97+
LaunchedEffect(windowInfo) {
98+
snapshotFlow { windowInfo.isWindowFocused }.collect { isWindowFocused ->
99+
if (isWindowFocused) {
100+
focusRequester.requestFocus()
101+
}
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)