@ -53,7 +53,7 @@ import com.tailscale.ipn.mdm.ShowHide
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.theme.AppTheme
import com.tailscale.ipn.ui.theme.AppTheme
import com.tailscale.ipn.ui.util.AndroidTVUtil
import com.tailscale.ipn.ui.util.AndroidTVUtil .isAndroidTV
import com.tailscale.ipn.ui.util.set
import com.tailscale.ipn.ui.util.set
import com.tailscale.ipn.ui.util.universalFit
import com.tailscale.ipn.ui.util.universalFit
import com.tailscale.ipn.ui.view.AboutView
import com.tailscale.ipn.ui.view.AboutView
@ -177,49 +177,53 @@ class MainActivity : ComponentActivity() {
}
}
viewModel . setVpnPermissionLauncher ( vpnPermissionLauncher )
viewModel . setVpnPermissionLauncher ( vpnPermissionLauncher )
val directoryPickerLauncher =
var directoryPickerLauncher : ActivityResultLauncher < Uri ? > ? = null
registerForActivityResult ( ActivityResultContracts . OpenDocumentTree ( ) ) { uri : Uri ? ->
if ( canOpenDocumentTree ( ) ) {
if ( uri != null ) {
directoryPickerLauncher =
try {
registerForActivityResult ( ActivityResultContracts . OpenDocumentTree ( ) ) { uri : Uri ? ->
// Try to take persistable permissions for both read and write.
if ( uri != null ) {
contentResolver . takePersistableUriPermission (
try {
uri ,
// Try to take persistable permissions for both read and write.
Intent . FLAG _GRANT _READ _URI _PERMISSION or Intent . FLAG _GRANT _WRITE _URI _PERMISSION )
contentResolver . takePersistableUriPermission (
} catch ( e : SecurityException ) {
uri ,
TSLog . e ( " MainActivity " , " Failed to persist permissions: $e " )
Intent . FLAG _GRANT _READ _URI _PERMISSION or Intent . FLAG _GRANT _WRITE _URI _PERMISSION )
}
} catch ( e : SecurityException ) {
TSLog . e ( " MainActivity " , " Failed to persist permissions: $e " )
}
// Check if write permission is actually granted.
// Check if write permission is actually granted.
val writePermission =
val writePermission =
this . checkUriPermission (
this . checkUriPermission (
uri , Process . myPid ( ) , Process . myUid ( ) , Intent . FLAG _GRANT _WRITE _URI _PERMISSION )
uri , Process . myPid ( ) , Process . myUid ( ) , Intent . FLAG _GRANT _WRITE _URI _PERMISSION )
if ( writePermission == PackageManager . PERMISSION _GRANTED ) {
if ( writePermission == PackageManager . PERMISSION _GRANTED ) {
TSLog . d ( " MainActivity " , " Write permission granted for $uri " )
TSLog . d ( " MainActivity " , " Write permission granted for $uri " )
lifecycleScope . launch ( Dispatchers . IO ) {
lifecycleScope . launch ( Dispatchers . IO ) {
try {
try {
Libtailscale . setDirectFileRoot ( uri . toString ( ) )
Libtailscale . setDirectFileRoot ( uri . toString ( ) )
TaildropDirectoryStore . saveFileDirectory ( uri )
TaildropDirectoryStore . saveFileDirectory ( uri )
permissionsViewModel . refreshCurrentDir ( )
permissionsViewModel . refreshCurrentDir ( )
} catch ( e : Exception ) {
} catch ( e : Exception ) {
TSLog . e ( " MainActivity " , " Failed to set Taildrop root: $e " )
TSLog . e ( " MainActivity " , " Failed to set Taildrop root: $e " )
}
}
}
} else {
TSLog . d (
" MainActivity " ,
" Write access not granted for $uri . Falling back to internal storage. " )
// Don't save directory URI and fall back to internal storage.
}
}
} else {
} else {
TSLog . d (
TSLog . d (
" MainActivity " ,
" MainActivity " ,
" Write access not granted for $uri . Falling back to internal storage. " )
" Taildrop directory not saved. Will fall back to internal storage. " )
// Don't save directory URI and fall back to internal storage.
}
} else {
TSLog . d (
" MainActivity " , " Taildrop directory not saved. Will fall back to internal storage. " )
// Fall back to internal storage.
// Fall back to internal storage.
}
}
}
}
viewModel . setDirectoryPickerLauncher ( directoryPickerLauncher )
viewModel . setDirectoryPickerLauncher ( directoryPickerLauncher )
}
setContent {
setContent {
navController = rememberNavController ( )
navController = rememberNavController ( )
@ -354,9 +358,11 @@ class MainActivity : ComponentActivity() {
{ navController . navigate ( " taildropDir " ) } ,
{ navController . navigate ( " taildropDir " ) } ,
{ navController . navigate ( " notifications " ) } )
{ navController . navigate ( " notifications " ) } )
}
}
composable ( " taildropDir " ) {
directoryPickerLauncher ?. let {
TaildropDirView (
val launcher = it
backTo ( " permissions " ) , directoryPickerLauncher , permissionsViewModel )
composable ( " taildropDir " ) {
TaildropDirView ( backTo ( " permissions " ) , launcher , permissionsViewModel )
}
}
}
composable ( " notifications " ) {
composable ( " notifications " ) {
NotificationsView ( backTo ( " permissions " ) , :: openApplicationSettings )
NotificationsView ( backTo ( " permissions " ) , :: openApplicationSettings )
@ -406,6 +412,16 @@ class MainActivity : ComponentActivity() {
lifecycleScope . launch { Notifier . loginFinished . collect { _ -> loginQRCode . set ( null ) } }
lifecycleScope . launch { Notifier . loginFinished . collect { _ -> loginQRCode . set ( null ) } }
}
}
// Most AndroidTV's don't support this and the UX is completely broken regardless. We have
// reports of some old devices throwing ActivityNotFound exceptions on TV as well, so we
// carefully guard against the attempt.
private fun Context . canOpenDocumentTree ( ) : Boolean {
return !is AndroidTV ( ) &&
Intent ( Intent . ACTION _OPEN _DOCUMENT _TREE )
. addCategory ( Intent . CATEGORY _DEFAULT )
. resolveActivity ( packageManager ) != null
}
private fun showOtherVPNConflictDialog ( ) {
private fun showOtherVPNConflictDialog ( ) {
AlertDialog . Builder ( this )
AlertDialog . Builder ( this )
. setTitle ( R . string . vpn _permission _denied )
. setTitle ( R . string . vpn _permission _denied )
@ -437,7 +453,7 @@ class MainActivity : ComponentActivity() {
// Returns true if we should render a QR code instead of launching a browser
// Returns true if we should render a QR code instead of launching a browser
// for login requests
// for login requests
private fun useQRCodeLogin ( ) : Boolean {
private fun useQRCodeLogin ( ) : Boolean {
return AndroidTVUtil . isAndroidTV ( )
return isAndroidTV ( )
}
}
override fun onNewIntent ( intent : Intent ) {
override fun onNewIntent ( intent : Intent ) {