@@ -14,8 +14,8 @@ import android.content.IntentFilter
14
14
import android.content.SharedPreferences
15
15
import android.content.pm.PackageManager
16
16
import android.net.ConnectivityManager
17
+ import android.net.Uri
17
18
import android.os.Build
18
- import android.os.Environment
19
19
import android.util.Log
20
20
import androidx.core.app.ActivityCompat
21
21
import androidx.core.app.NotificationCompat
@@ -36,12 +36,8 @@ import com.tailscale.ipn.ui.notifier.Notifier
36
36
import com.tailscale.ipn.ui.viewModel.VpnViewModel
37
37
import com.tailscale.ipn.ui.viewModel.VpnViewModelFactory
38
38
import com.tailscale.ipn.util.FeatureFlags
39
+ import com.tailscale.ipn.util.ShareFileHelper
39
40
import com.tailscale.ipn.util.TSLog
40
- import java.io.File
41
- import java.io.IOException
42
- import java.net.NetworkInterface
43
- import java.security.GeneralSecurityException
44
- import java.util.Locale
45
41
import kotlinx.coroutines.CoroutineScope
46
42
import kotlinx.coroutines.Dispatchers
47
43
import kotlinx.coroutines.SupervisorJob
@@ -53,12 +49,18 @@ import kotlinx.coroutines.launch
53
49
import kotlinx.serialization.encodeToString
54
50
import kotlinx.serialization.json.Json
55
51
import libtailscale.Libtailscale
52
+ import java.io.IOException
53
+ import java.net.NetworkInterface
54
+ import java.security.GeneralSecurityException
55
+ import java.util.Locale
56
56
57
57
class App : UninitializedApp (), libtailscale.AppContext, ViewModelStoreOwner {
58
58
val applicationScope = CoroutineScope (SupervisorJob () + Dispatchers .Default )
59
59
60
60
companion object {
61
61
private const val FILE_CHANNEL_ID = " tailscale-files"
62
+ // Key to store the SAF URI in EncryptedSharedPreferences.
63
+ private val PREF_KEY_SAF_URI = " saf_directory_uri"
62
64
private const val TAG = " App"
63
65
private lateinit var appInstance: App
64
66
@@ -150,17 +152,13 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
150
152
}
151
153
152
154
private fun initializeApp () {
153
- val dataDir = this .filesDir.absolutePath
154
-
155
- // Set this to enable direct mode for taildrop whereby downloads will be saved directly
156
- // to the given folder. We will preferentially use <shared>/Downloads and fallback to
157
- // an app local directory "Taildrop" if we cannot create that. This mode does not support
158
- // user notifications for incoming files.
159
- val directFileDir = this .prepareDownloadsFolder()
160
- app = Libtailscale .start(dataDir, directFileDir.absolutePath, this )
161
- Request .setApp(app)
162
- Notifier .setApp(app)
163
- Notifier .start(applicationScope)
155
+ // Check if a directory URI has already been stored.
156
+ val storedUri = getStoredDirectoryUri()
157
+ if (storedUri != null && storedUri.toString().startsWith(" content://" )) {
158
+ startLibtailscale(storedUri.toString())
159
+ } else {
160
+ startLibtailscale(this .getFilesDir().absolutePath)
161
+ }
164
162
healthNotifier = HealthNotifier (Notifier .health, Notifier .state, applicationScope)
165
163
connectivityManager = this .getSystemService(Context .CONNECTIVITY_SERVICE ) as ConnectivityManager
166
164
NetworkChangeCallback .monitorDnsChanges(connectivityManager, dns)
@@ -205,6 +203,18 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
205
203
FeatureFlags .initialize(mapOf (" enable_new_search" to true ))
206
204
}
207
205
206
+ /* *
207
+ * Called when a SAF directory URI is available (either already stored or chosen). We must restart
208
+ * Tailscale because directFileRoot must be set before LocalBackend starts being used.
209
+ */
210
+ fun startLibtailscale (directFileRoot : String ) {
211
+ ShareFileHelper .init (this , directFileRoot)
212
+ app = Libtailscale .start(this .filesDir.absolutePath, directFileRoot, this )
213
+ Request .setApp(app)
214
+ Notifier .setApp(app)
215
+ Notifier .start(applicationScope)
216
+ }
217
+
208
218
private fun initViewModels () {
209
219
vpnViewModel = ViewModelProvider (this , VpnViewModelFactory (this )).get(VpnViewModel ::class .java)
210
220
}
@@ -247,6 +257,11 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
247
257
EncryptedSharedPreferences .PrefValueEncryptionScheme .AES256_GCM )
248
258
}
249
259
260
+ fun getStoredDirectoryUri (): Uri ? {
261
+ val uriString = getEncryptedPrefs().getString(PREF_KEY_SAF_URI , null )
262
+ return uriString?.let { Uri .parse(it) }
263
+ }
264
+
250
265
/*
251
266
* setAbleToStartVPN remembers whether or not we're able to start the VPN
252
267
* by storing this in a shared preference. This allows us to check this
@@ -310,29 +325,6 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
310
325
return sb.toString()
311
326
}
312
327
313
- private fun prepareDownloadsFolder (): File {
314
- var downloads = Environment .getExternalStoragePublicDirectory(Environment .DIRECTORY_DOWNLOADS )
315
-
316
- try {
317
- if (! downloads.exists()) {
318
- downloads.mkdirs()
319
- }
320
- } catch (e: Exception ) {
321
- TSLog .e(TAG , " Failed to create downloads folder: $e " )
322
- downloads = File (this .filesDir, " Taildrop" )
323
- try {
324
- if (! downloads.exists()) {
325
- downloads.mkdirs()
326
- }
327
- } catch (e: Exception ) {
328
- TSLog .e(TAG , " Failed to create Taildrop folder: $e " )
329
- downloads = File (" " )
330
- }
331
- }
332
-
333
- return downloads
334
- }
335
-
336
328
@Throws(
337
329
IOException ::class , GeneralSecurityException ::class , MDMSettings .NoSuchKeyException ::class )
338
330
override fun getSyspolicyBooleanValue (key : String ): Boolean {
0 commit comments