android: skip SAF directory picker if unsupported

updates tailscale/corp#30254

This adds an upfront isAndroidTV check before we set the
SAF directory picker activity.  We'll check both that we can
actually launch the activity and that this isn't and AndroidTV
device where the directory picker activity isn't supported or
has a tendency to throw ActivityNotFound exceptions.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
jonathan/tv-dirsel
Jonathan Nobels 5 months ago
parent e5a704f785
commit aa8f16baef

@ -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,7 +177,9 @@ class MainActivity : ComponentActivity() {
} }
viewModel.setVpnPermissionLauncher(vpnPermissionLauncher) viewModel.setVpnPermissionLauncher(vpnPermissionLauncher)
val directoryPickerLauncher = var directoryPickerLauncher: ActivityResultLauncher<Uri?>? = null
if (canOpenDocumentTree()) {
directoryPickerLauncher =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri: Uri? -> registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri: Uri? ->
if (uri != null) { if (uri != null) {
try { try {
@ -213,13 +215,15 @@ class MainActivity : ComponentActivity() {
} }
} else { } else {
TSLog.d( TSLog.d(
"MainActivity", "Taildrop directory not saved. Will fall back to internal storage.") "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") })
} }
directoryPickerLauncher?.let {
val launcher = it
composable("taildropDir") { composable("taildropDir") {
TaildropDirView( TaildropDirView(backTo("permissions"), launcher, permissionsViewModel)
backTo("permissions"), directoryPickerLauncher, 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 !isAndroidTV() &&
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) {

@ -221,8 +221,10 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() {
} }
fun showDirectoryPickerLauncher() { fun showDirectoryPickerLauncher() {
directoryPickerLauncher?.let {
_showDirectoryPickerInterstitial.set(false) _showDirectoryPickerInterstitial.set(false)
directoryPickerLauncher?.launch(null) it.launch(null)
}
} }
fun checkIfTaildropDirectorySelected() { fun checkIfTaildropDirectorySelected() {
@ -233,17 +235,23 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() {
val app = App.get() val app = App.get()
val storedUri = app.getStoredDirectoryUri() val storedUri = app.getStoredDirectoryUri()
if (storedUri == null) { if (storedUri == null) {
if (directoryPickerLauncher != null) {
// No stored URI, so launch the directory picker. // No stored URI, so launch the directory picker.
_showDirectoryPickerInterstitial.set(true) _showDirectoryPickerInterstitial.set(true)
}
return return
} }
val documentFile = DocumentFile.fromTreeUri(app, storedUri) val documentFile = DocumentFile.fromTreeUri(app, storedUri)
if (documentFile == null || !documentFile.exists() || !documentFile.canWrite()) { if (documentFile == null || !documentFile.exists() || !documentFile.canWrite()) {
if (directoryPickerLauncher != null) {
TSLog.d( TSLog.d(
"MainViewModel", "MainViewModel",
"Stored directory URI is invalid or inaccessible; launching directory picker.") "Stored directory URI is invalid or inaccessible; launching directory picker.")
_showDirectoryPickerInterstitial.set(true) _showDirectoryPickerInterstitial.set(true)
} else {
TSLog.d("MainViewModel", "Directory picker activity not available")
}
} else { } else {
TSLog.d("MainViewModel", "Using stored directory URI: $storedUri") TSLog.d("MainViewModel", "Using stored directory URI: $storedUri")
} }

Loading…
Cancel
Save