From cae67a40f54b6b2824181fc228c795b9cff3eb74 Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Fri, 27 Mar 2026 15:18:17 -0400 Subject: [PATCH] android: add back button to taildrop share activity fixes tailscale/corp#39202 Adds a back button to the taildrop share activity. Signed-off-by: Jonathan Nobels --- .../java/com/tailscale/ipn/ShareActivity.kt | 32 +++++- .../com/tailscale/ipn/ui/view/TaildropView.kt | 107 ++++++++++-------- 2 files changed, 88 insertions(+), 51 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ShareActivity.kt b/android/src/main/java/com/tailscale/ipn/ShareActivity.kt index 09d9665ee3..299f7d964f 100644 --- a/android/src/main/java/com/tailscale/ipn/ShareActivity.kt +++ b/android/src/main/java/com/tailscale/ipn/ShareActivity.kt @@ -11,9 +11,19 @@ import android.provider.OpenableColumns import android.webkit.MimeTypeMap import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.lifecycle.lifecycleScope import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.theme.AppTheme @@ -21,12 +31,12 @@ import com.tailscale.ipn.ui.util.set import com.tailscale.ipn.ui.util.universalFit import com.tailscale.ipn.ui.view.TaildropView import com.tailscale.ipn.util.TSLog -import kotlin.random.Random import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlin.random.Random // ShareActivity is the entry point for Taildrop share intents class ShareActivity : ComponentActivity() { @@ -34,13 +44,31 @@ class ShareActivity : ComponentActivity() { private val requestedTransfers: StateFlow> = MutableStateFlow(emptyList()) + @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { AppTheme { Surface(color = MaterialTheme.colorScheme.inverseSurface) { // Background for the letterbox Surface(modifier = Modifier.universalFit()) { - TaildropView(requestedTransfers, (application as App).applicationScope) + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.share)) }, + navigationIcon = { + IconButton(onClick = { finish() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back), + ) + } + }, + ) + }) { innerPadding -> + Surface(modifier = Modifier.padding(innerPadding)) { + TaildropView(requestedTransfers, (application as App).applicationScope) + } + } } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/TaildropView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/TaildropView.kt index f440fac04a..b246a55e4f 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/TaildropView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/TaildropView.kt @@ -9,15 +9,12 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -50,7 +47,7 @@ fun TaildropView( requestedTransfers: StateFlow>, applicationScope: CoroutineScope, viewModel: TaildropViewModel = - viewModel(factory = TaildropViewModelFactory(requestedTransfers, applicationScope)) + viewModel(factory = TaildropViewModelFactory(requestedTransfers, applicationScope)), ) { val TAG = "TaildropView" val focusRequester = remember { FocusRequester() } @@ -64,29 +61,28 @@ fun TaildropView( } } - Scaffold(contentWindowInsets = WindowInsets.statusBars, topBar = { Header(R.string.share) }) { - paddingInsets -> - Column(modifier = Modifier.focusRequester(focusRequester).focusable().padding(paddingInsets)) { - val showDialog = viewModel.showDialog.collectAsState().value + Column(modifier = Modifier.focusRequester(focusRequester).focusable()) { + val showDialog = viewModel.showDialog.collectAsState().value - showDialog?.let { ErrorDialog(type = it, action = { viewModel.showDialog.set(null) }) } + showDialog?.let { ErrorDialog(type = it, action = { viewModel.showDialog.set(null) }) } - FileShareHeader( - fileTransfers = requestedTransfers.collectAsState().value, - totalSize = viewModel.totalSize) + FileShareHeader( + fileTransfers = requestedTransfers.collectAsState().value, + totalSize = viewModel.totalSize, + ) - when (viewModel.state.collectAsState().value) { - Ipn.State.Running -> { - val peers by viewModel.myPeers.collectAsState() - val context = LocalContext.current - FileSharePeerList( - peers = peers, - stateViewGenerator = { peerId -> viewModel.TrailingContentForPeer(peerId = peerId) }, - onShare = { viewModel.share(context, it) }) - } - else -> { - FileShareConnectView { viewModel.startVPN() } - } + when (viewModel.state.collectAsState().value) { + Ipn.State.Running -> { + val peers by viewModel.myPeers.collectAsState() + val context = LocalContext.current + FileSharePeerList( + peers = peers, + stateViewGenerator = { peerId -> viewModel.TrailingContentForPeer(peerId = peerId) }, + onShare = { viewModel.share(context, it) }, + ) + } + else -> { + FileShareConnectView { viewModel.startVPN() } } } } @@ -96,7 +92,7 @@ fun TaildropView( fun FileSharePeerList( peers: List, stateViewGenerator: @Composable (String) -> Unit, - onShare: (Tailcfg.Node) -> Unit + onShare: (Tailcfg.Node) -> Unit, ) { SectionDivider(stringResource(R.string.my_devices)) @@ -105,11 +101,13 @@ fun FileSharePeerList( Column( modifier = Modifier.padding(horizontal = 8.dp).fillMaxHeight(), verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally) { - Text( - stringResource(R.string.no_devices_to_share_with), - style = MaterialTheme.typography.titleMedium) - } + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + stringResource(R.string.no_devices_to_share_with), + style = MaterialTheme.typography.titleMedium, + ) + } } false -> { LazyColumn { @@ -119,7 +117,8 @@ fun FileSharePeerList( peer = peer, onClick = { onShare(peer) }, subtitle = { peer.Hostinfo.OS ?: "" }, - trailingContent = { stateViewGenerator(peer.StableID) }) + trailingContent = { stateViewGenerator(peer.StableID) }, + ) } } } @@ -132,17 +131,20 @@ fun FileShareConnectView(onToggle: () -> Unit) { Column( modifier = Modifier.padding(horizontal = 16.dp).fillMaxHeight(), verticalArrangement = Arrangement.spacedBy(6.dp, alignment = Alignment.CenterVertically), - horizontalAlignment = Alignment.CenterHorizontally) { - Text( - stringResource(R.string.connect_to_your_tailnet_to_share_files), - style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.size(1.dp)) - PrimaryActionButton(onClick = onToggle) { - Text( - text = stringResource(id = R.string.connect), - fontSize = MaterialTheme.typography.titleMedium.fontSize) - } - } + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + stringResource(R.string.connect_to_your_tailnet_to_share_files), + style = MaterialTheme.typography.titleMedium, + ) + Spacer(modifier = Modifier.size(1.dp)) + PrimaryActionButton(onClick = onToggle) { + Text( + text = stringResource(id = R.string.connect), + fontSize = MaterialTheme.typography.titleMedium.fontSize, + ) + } + } } @Composable @@ -155,7 +157,8 @@ fun FileShareHeader(fileTransfers: List, totalSize: Long) { true -> Text( stringResource(R.string.no_files_to_share), - style = MaterialTheme.typography.titleMedium) + style = MaterialTheme.typography.titleMedium, + ) false -> { when (fileTransfers.size) { @@ -163,7 +166,8 @@ fun FileShareHeader(fileTransfers: List, totalSize: Long) { else -> Text( stringResource(R.string.file_count, fileTransfers.size), - style = MaterialTheme.typography.titleMedium) + style = MaterialTheme.typography.titleMedium, + ) } } } @@ -171,7 +175,8 @@ fun FileShareHeader(fileTransfers: List, totalSize: Long) { Text( size, style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.secondary) + color = MaterialTheme.colorScheme.secondary, + ) } } } @@ -185,7 +190,8 @@ fun IconForTransfer(transfers: List) { Icon( painter = painterResource(R.drawable.warning), contentDescription = "no files", - modifier = Modifier.size(32.dp)) + modifier = Modifier.size(32.dp), + ) 1 -> { // Show a thumbnail for single image shares. val context = LocalContext.current @@ -194,20 +200,23 @@ fun IconForTransfer(transfers: List) { AsyncImage( model = transfers[0].uri, contentDescription = "one file", - modifier = Modifier.size(40.dp)) + modifier = Modifier.size(40.dp), + ) return } Icon( painter = painterResource(R.drawable.single_file), contentDescription = "files", - modifier = Modifier.size(40.dp)) + modifier = Modifier.size(40.dp), + ) } } else -> Icon( painter = painterResource(R.drawable.single_file), contentDescription = "files", - modifier = Modifier.size(40.dp)) + modifier = Modifier.size(40.dp), + ) } }