@@ -53,7 +53,7 @@ import com.tailscale.ipn.mdm.ShowHide
53
53
import com.tailscale.ipn.ui.model.Ipn
54
54
import com.tailscale.ipn.ui.notifier.Notifier
55
55
import com.tailscale.ipn.ui.theme.AppTheme
56
- import com.tailscale.ipn.ui.util.AndroidTVUtil
56
+ import com.tailscale.ipn.ui.util.AndroidTVUtil.isAndroidTV
57
57
import com.tailscale.ipn.ui.util.set
58
58
import com.tailscale.ipn.ui.util.universalFit
59
59
import com.tailscale.ipn.ui.view.AboutView
@@ -177,49 +177,53 @@ class MainActivity : ComponentActivity() {
177
177
}
178
178
viewModel.setVpnPermissionLauncher(vpnPermissionLauncher)
179
179
180
- val directoryPickerLauncher =
181
- registerForActivityResult(ActivityResultContracts .OpenDocumentTree ()) { uri: Uri ? ->
182
- if (uri != null ) {
183
- try {
184
- // Try to take persistable permissions for both read and write.
185
- contentResolver.takePersistableUriPermission(
186
- uri,
187
- Intent .FLAG_GRANT_READ_URI_PERMISSION or Intent .FLAG_GRANT_WRITE_URI_PERMISSION )
188
- } catch (e: SecurityException ) {
189
- TSLog .e(" MainActivity" , " Failed to persist permissions: $e " )
190
- }
180
+ var directoryPickerLauncher: ActivityResultLauncher <Uri ?>? = null
181
+ if (canOpenDocumentTree()) {
182
+ directoryPickerLauncher =
183
+ registerForActivityResult(ActivityResultContracts .OpenDocumentTree ()) { uri: Uri ? ->
184
+ if (uri != null ) {
185
+ try {
186
+ // Try to take persistable permissions for both read and write.
187
+ contentResolver.takePersistableUriPermission(
188
+ uri,
189
+ Intent .FLAG_GRANT_READ_URI_PERMISSION or Intent .FLAG_GRANT_WRITE_URI_PERMISSION )
190
+ } catch (e: SecurityException ) {
191
+ TSLog .e(" MainActivity" , " Failed to persist permissions: $e " )
192
+ }
191
193
192
- // Check if write permission is actually granted.
193
- val writePermission =
194
- this .checkUriPermission(
195
- uri, Process .myPid(), Process .myUid(), Intent .FLAG_GRANT_WRITE_URI_PERMISSION )
196
- if (writePermission == PackageManager .PERMISSION_GRANTED ) {
197
- TSLog .d(" MainActivity" , " Write permission granted for $uri " )
198
-
199
- lifecycleScope.launch(Dispatchers .IO ) {
200
- try {
201
- Libtailscale .setDirectFileRoot(uri.toString())
202
- TaildropDirectoryStore .saveFileDirectory(uri)
203
- permissionsViewModel.refreshCurrentDir()
204
- } catch (e: Exception ) {
205
- TSLog .e(" MainActivity" , " Failed to set Taildrop root: $e " )
194
+ // Check if write permission is actually granted.
195
+ val writePermission =
196
+ this .checkUriPermission(
197
+ uri, Process .myPid(), Process .myUid(), Intent .FLAG_GRANT_WRITE_URI_PERMISSION )
198
+ if (writePermission == PackageManager .PERMISSION_GRANTED ) {
199
+ TSLog .d(" MainActivity" , " Write permission granted for $uri " )
200
+
201
+ lifecycleScope.launch(Dispatchers .IO ) {
202
+ try {
203
+ Libtailscale .setDirectFileRoot(uri.toString())
204
+ TaildropDirectoryStore .saveFileDirectory(uri)
205
+ permissionsViewModel.refreshCurrentDir()
206
+ } catch (e: Exception ) {
207
+ TSLog .e(" MainActivity" , " Failed to set Taildrop root: $e " )
208
+ }
206
209
}
210
+ } else {
211
+ TSLog .d(
212
+ " MainActivity" ,
213
+ " Write access not granted for $uri . Falling back to internal storage." )
214
+ // Don't save directory URI and fall back to internal storage.
207
215
}
208
216
} else {
209
217
TSLog .d(
210
218
" MainActivity" ,
211
- " Write access not granted for $uri . Falling back to internal storage." )
212
- // Don't save directory URI and fall back to internal storage.
213
- }
214
- } else {
215
- TSLog .d(
216
- " MainActivity" , " Taildrop directory not saved. Will fall back to internal storage." )
219
+ " Taildrop directory not saved. Will fall back to internal storage." )
217
220
218
- // Fall back to internal storage.
221
+ // Fall back to internal storage.
222
+ }
219
223
}
220
- }
221
224
222
- viewModel.setDirectoryPickerLauncher(directoryPickerLauncher)
225
+ viewModel.setDirectoryPickerLauncher(directoryPickerLauncher)
226
+ }
223
227
224
228
setContent {
225
229
navController = rememberNavController()
@@ -354,9 +358,11 @@ class MainActivity : ComponentActivity() {
354
358
{ navController.navigate(" taildropDir" ) },
355
359
{ navController.navigate(" notifications" ) })
356
360
}
357
- composable(" taildropDir" ) {
358
- TaildropDirView (
359
- backTo(" permissions" ), directoryPickerLauncher, permissionsViewModel)
361
+ directoryPickerLauncher?.let {
362
+ val launcher = it
363
+ composable(" taildropDir" ) {
364
+ TaildropDirView (backTo(" permissions" ), launcher, permissionsViewModel)
365
+ }
360
366
}
361
367
composable(" notifications" ) {
362
368
NotificationsView (backTo(" permissions" ), ::openApplicationSettings)
@@ -406,6 +412,16 @@ class MainActivity : ComponentActivity() {
406
412
lifecycleScope.launch { Notifier .loginFinished.collect { _ -> loginQRCode.set(null ) } }
407
413
}
408
414
415
+ // Most AndroidTV's don't support this and the UX is completely broken regardless. We have
416
+ // reports of some old devices throwing ActivityNotFound exceptions on TV as well, so we
417
+ // carefully guard against the attempt.
418
+ private fun Context.canOpenDocumentTree (): Boolean {
419
+ return ! isAndroidTV() &&
420
+ Intent (Intent .ACTION_OPEN_DOCUMENT_TREE )
421
+ .addCategory(Intent .CATEGORY_DEFAULT )
422
+ .resolveActivity(packageManager) != null
423
+ }
424
+
409
425
private fun showOtherVPNConflictDialog () {
410
426
AlertDialog .Builder (this )
411
427
.setTitle(R .string.vpn_permission_denied)
@@ -437,7 +453,7 @@ class MainActivity : ComponentActivity() {
437
453
// Returns true if we should render a QR code instead of launching a browser
438
454
// for login requests
439
455
private fun useQRCodeLogin (): Boolean {
440
- return AndroidTVUtil . isAndroidTV()
456
+ return isAndroidTV()
441
457
}
442
458
443
459
override fun onNewIntent (intent : Intent ) {
0 commit comments