@@ -6,6 +6,7 @@ package com.tailscale.ipn
6
6
import android.annotation.SuppressLint
7
7
import android.app.Activity
8
8
import android.app.AlertDialog
9
+ import android.content.BroadcastReceiver
9
10
import android.content.Context
10
11
import android.content.Intent
11
12
import android.content.RestrictionsManager
@@ -14,14 +15,15 @@ import android.content.res.Configuration.SCREENLAYOUT_SIZE_LARGE
14
15
import android.content.res.Configuration.SCREENLAYOUT_SIZE_MASK
15
16
import android.net.ConnectivityManager
16
17
import android.net.NetworkCapabilities
18
+ import android.net.Uri
17
19
import android.os.Build
18
20
import android.os.Bundle
19
21
import android.provider.Settings
20
- import android.util.Log
21
22
import androidx.activity.ComponentActivity
22
23
import androidx.activity.compose.setContent
23
24
import androidx.activity.result.ActivityResultLauncher
24
25
import androidx.activity.result.contract.ActivityResultContract
26
+ import androidx.activity.result.contract.ActivityResultContracts
25
27
import androidx.annotation.RequiresApi
26
28
import androidx.browser.customtabs.CustomTabsIntent
27
29
import androidx.compose.animation.core.LinearOutSlowInEasing
@@ -89,8 +91,14 @@ import kotlinx.coroutines.cancel
89
91
import kotlinx.coroutines.flow.MutableStateFlow
90
92
import kotlinx.coroutines.flow.StateFlow
91
93
import kotlinx.coroutines.launch
94
+ import libtailscale.Libtailscale
95
+ import java.io.IOException
96
+ import java.security.GeneralSecurityException
92
97
93
98
class MainActivity : ComponentActivity () {
99
+ // Key to store the SAF URI in EncryptedSharedPreferences.
100
+ private val PREF_KEY_SAF_URI = " saf_directory_uri"
101
+ lateinit var safUriReceiver: BroadcastReceiver
94
102
private lateinit var navController: NavHostController
95
103
private lateinit var vpnPermissionLauncher: ActivityResultLauncher <Intent >
96
104
private val viewModel: MainViewModel by lazy {
@@ -150,6 +158,24 @@ class MainActivity : ComponentActivity() {
150
158
}
151
159
viewModel.setVpnPermissionLauncher(vpnPermissionLauncher)
152
160
161
+ val directoryPickerLauncher =
162
+ registerForActivityResult(ActivityResultContracts .OpenDocumentTree ()) { uri: Uri ? ->
163
+ if (uri != null ) {
164
+ // Persist permissions for future access.
165
+ contentResolver.takePersistableUriPermission(
166
+ uri,
167
+ Intent .FLAG_GRANT_READ_URI_PERMISSION or Intent .FLAG_GRANT_WRITE_URI_PERMISSION )
168
+ // Set the directory to download files to directly.
169
+ Libtailscale .setDirectFileRoot(uri.toString())
170
+ saveFileDirectory(uri)
171
+ } else {
172
+ TSLog .d(
173
+ " MainActivity" , " Taildrop directory not saved. Will fall back to internal storage." )
174
+ }
175
+ }
176
+
177
+ viewModel.setDirectoryPickerLauncher(directoryPickerLauncher)
178
+
153
179
setContent {
154
180
navController = rememberNavController()
155
181
@@ -198,7 +224,7 @@ class MainActivity : ComponentActivity() {
198
224
onNavigateToSearch = {
199
225
viewModel.enableSearchAutoFocus()
200
226
navController.navigate(" search" )
201
- })
227
+ })
202
228
203
229
val settingsNav =
204
230
SettingsNav (
@@ -245,9 +271,8 @@ class MainActivity : ComponentActivity() {
245
271
viewModel = viewModel,
246
272
navController = navController,
247
273
onNavigateBack = { navController.popBackStack() },
248
- autoFocus = autoFocus
249
- )
250
- }
274
+ autoFocus = autoFocus)
275
+ }
251
276
composable(" settings" ) { SettingsView (settingsNav) }
252
277
composable(" exitNodes" ) { ExitNodePicker (exitNodePickerNav) }
253
278
composable(" health" ) { HealthView (backTo(" main" )) }
@@ -365,23 +390,28 @@ class MainActivity : ComponentActivity() {
365
390
override fun onNewIntent (intent : Intent ) {
366
391
super .onNewIntent(intent)
367
392
if (intent.getBooleanExtra(START_AT_ROOT , false )) {
368
- if (this ::navController.isInitialized) {
369
- val previousEntry = navController.previousBackStackEntry
370
- TSLog .d(" MainActivity" , " onNewIntent: previousBackStackEntry = $previousEntry " )
371
-
372
- if (previousEntry != null ) {
373
- navController.popBackStack(route = " main" , inclusive = false )
374
- } else {
375
- TSLog .e(" MainActivity" , " onNewIntent: No previous back stack entry, navigating directly to 'main'" )
376
- navController.navigate(" main" ) {
377
- popUpTo(" main" ) { inclusive = true }
378
- }
379
- }
393
+ if (this ::navController.isInitialized) {
394
+ val previousEntry = navController.previousBackStackEntry
395
+ TSLog .d(" MainActivity" , " onNewIntent: previousBackStackEntry = $previousEntry " )
396
+
397
+ if (previousEntry != null ) {
398
+ navController.popBackStack(route = " main" , inclusive = false )
399
+ } else {
400
+ TSLog .e(
401
+ " MainActivity" ,
402
+ " onNewIntent: No previous back stack entry, navigating directly to 'main'" )
403
+ navController.navigate(" main" ) { popUpTo(" main" ) { inclusive = true } }
380
404
}
405
+ }
381
406
}
382
- }
383
-
407
+ }
384
408
409
+ @Throws(IOException ::class , GeneralSecurityException ::class )
410
+ fun saveFileDirectory (directoryUri : Uri ) {
411
+ val prefs = App .get().getEncryptedPrefs()
412
+ prefs.edit().putString(PREF_KEY_SAF_URI , directoryUri.toString()).apply ()
413
+ App .get().startLibtailscale(directoryUri.toString())
414
+ }
385
415
386
416
private fun login (urlString : String ) {
387
417
// Launch coroutine to listen for state changes. When the user completes login, relaunch
0 commit comments