@@ -34,10 +34,18 @@ import androidx.compose.animation.fadeIn
3434import androidx.compose.animation.fadeOut
3535import androidx.compose.animation.slideInHorizontally
3636import androidx.compose.animation.slideOutHorizontally
37+ import androidx.compose.material3.AlertDialog
3738import androidx.compose.material3.MaterialTheme
3839import androidx.compose.material3.Surface
40+ import androidx.compose.material3.Text
41+ import androidx.compose.runtime.LaunchedEffect
3942import androidx.compose.runtime.collectAsState
43+ import androidx.compose.runtime.getValue
44+ import androidx.compose.runtime.mutableStateOf
45+ import androidx.compose.runtime.remember
46+ import androidx.compose.runtime.setValue
4047import androidx.compose.ui.Modifier
48+ import androidx.compose.ui.res.stringResource
4149import androidx.core.net.toUri
4250import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
4351import androidx.lifecycle.ViewModelProvider
@@ -75,39 +83,38 @@ import com.tailscale.ipn.ui.view.MullvadInfoView
7583import com.tailscale.ipn.ui.view.NotificationsView
7684import com.tailscale.ipn.ui.view.PeerDetails
7785import com.tailscale.ipn.ui.view.PermissionsView
86+ import com.tailscale.ipn.ui.view.PrimaryActionButton
7887import com.tailscale.ipn.ui.view.RunExitNodeView
7988import com.tailscale.ipn.ui.view.SearchView
8089import com.tailscale.ipn.ui.view.SettingsView
8190import com.tailscale.ipn.ui.view.SplitTunnelAppPickerView
8291import com.tailscale.ipn.ui.view.SubnetRoutingView
8392import com.tailscale.ipn.ui.view.TaildropDirView
93+ import com.tailscale.ipn.ui.view.TaildropDirectoryPickerPrompt
8494import com.tailscale.ipn.ui.view.TailnetLockSetupView
8595import com.tailscale.ipn.ui.view.UserSwitcherNav
8696import com.tailscale.ipn.ui.view.UserSwitcherView
97+ import com.tailscale.ipn.ui.viewModel.AppViewModel
8798import com.tailscale.ipn.ui.viewModel.ExitNodePickerNav
8899import com.tailscale.ipn.ui.viewModel.MainViewModel
89100import com.tailscale.ipn.ui.viewModel.MainViewModelFactory
90101import com.tailscale.ipn.ui.viewModel.PermissionsViewModel
91102import com.tailscale.ipn.ui.viewModel.PingViewModel
92103import com.tailscale.ipn.ui.viewModel.SettingsNav
93- import com.tailscale.ipn.ui.viewModel.VpnViewModel
104+ import com.tailscale.ipn.util.ShareFileHelper
94105import com.tailscale.ipn.util.TSLog
95106import kotlinx.coroutines.Dispatchers
96107import kotlinx.coroutines.cancel
97108import kotlinx.coroutines.flow.MutableStateFlow
98109import kotlinx.coroutines.flow.StateFlow
99110import kotlinx.coroutines.launch
100- import libtailscale.Libtailscale
101111
102112class MainActivity : ComponentActivity () {
103113 private lateinit var navController: NavHostController
104114 private lateinit var vpnPermissionLauncher: ActivityResultLauncher <Intent >
105- private val viewModel: MainViewModel by lazy {
106- val app = App .get()
107- vpnViewModel = app.getAppScopedViewModel()
108- ViewModelProvider (this , MainViewModelFactory (vpnViewModel)).get(MainViewModel ::class .java)
109- }
110- private lateinit var vpnViewModel: VpnViewModel
115+ private lateinit var appViewModel: AppViewModel
116+ private lateinit var viewModel: MainViewModel
117+
111118 val permissionsViewModel: PermissionsViewModel by viewModels()
112119
113120 companion object {
@@ -119,7 +126,6 @@ class MainActivity : ComponentActivity() {
119126 return (resources.configuration.screenLayout and SCREENLAYOUT_SIZE_MASK ) >=
120127 SCREENLAYOUT_SIZE_LARGE
121128 }
122-
123129 // The loginQRCode is used to track whether or not we should be rendering a QR code
124130 // to the user. This is used only on TV platforms with no browser in lieu of
125131 // simply opening the URL. This should be consumed once it has been handled.
@@ -132,37 +138,35 @@ class MainActivity : ComponentActivity() {
132138
133139 // grab app to make sure it initializes
134140 App .get()
135- vpnViewModel = ViewModelProvider (App .get()).get(VpnViewModel ::class .java)
141+ appViewModel = (application as App ).getAppScopedViewModel()
142+ viewModel =
143+ ViewModelProvider (this , MainViewModelFactory (appViewModel)).get(MainViewModel ::class .java)
136144
137145 val rm = getSystemService(Context .RESTRICTIONS_SERVICE ) as RestrictionsManager
138146 MDMSettings .update(App .get(), rm)
139-
140147 if (MDMSettings .onboardingFlow.flow.value.value == ShowHide .Hide ||
141148 MDMSettings .authKey.flow.value.value != null ) {
142149 setIntroScreenViewed(true )
143150 }
144-
145151 // (jonathan) TODO: Force the app to be portrait on small screens until we have
146152 // proper landscape layout support
147153 if (! isLandscapeCapable()) {
148154 requestedOrientation = ActivityInfo .SCREEN_ORIENTATION_PORTRAIT
149155 }
150-
151156 installSplashScreen()
152-
153157 vpnPermissionLauncher =
154158 registerForActivityResult(VpnPermissionContract ()) { granted ->
155159 if (granted) {
156160 TSLog .d(" VpnPermission" , " VPN permission granted" )
157- vpnViewModel .setVpnPrepared(true )
161+ appViewModel .setVpnPrepared(true )
158162 App .get().startVPN()
159163 } else {
160164 if (isAnotherVpnActive(this )) {
161165 TSLog .d(" VpnPermission" , " Another VPN is likely active" )
162166 showOtherVPNConflictDialog()
163167 } else {
164168 TSLog .d(" VpnPermission" , " Permission was denied by the user" )
165- vpnViewModel .setVpnPrepared(false )
169+ appViewModel .setVpnPrepared(false )
166170
167171 AlertDialog .Builder (this )
168172 .setTitle(R .string.vpn_permission_needed)
@@ -176,7 +180,6 @@ class MainActivity : ComponentActivity() {
176180 }
177181 }
178182 viewModel.setVpnPermissionLauncher(vpnPermissionLauncher)
179-
180183 val directoryPickerLauncher =
181184 registerForActivityResult(ActivityResultContracts .OpenDocumentTree ()) { uri: Uri ? ->
182185 if (uri != null ) {
@@ -188,7 +191,6 @@ class MainActivity : ComponentActivity() {
188191 } catch (e: SecurityException ) {
189192 TSLog .e(" MainActivity" , " Failed to persist permissions: $e " )
190193 }
191-
192194 // Check if write permission is actually granted.
193195 val writePermission =
194196 this .checkUriPermission(
@@ -198,9 +200,10 @@ class MainActivity : ComponentActivity() {
198200
199201 lifecycleScope.launch(Dispatchers .IO ) {
200202 try {
201- Libtailscale .setDirectFileRoot(uri.toString())
202203 TaildropDirectoryStore .saveFileDirectory(uri)
203204 permissionsViewModel.refreshCurrentDir()
205+ ShareFileHelper .notifyDirectoryReady()
206+ ShareFileHelper .setUri(uri.toString())
204207 } catch (e: Exception ) {
205208 TSLog .e(" MainActivity" , " Failed to set Taildrop root: $e " )
206209 }
@@ -214,14 +217,40 @@ class MainActivity : ComponentActivity() {
214217 } else {
215218 TSLog .d(
216219 " MainActivity" , " Taildrop directory not saved. Will fall back to internal storage." )
217-
218220 // Fall back to internal storage.
219221 }
220222 }
221223
222- viewModel.setDirectoryPickerLauncher( directoryPickerLauncher)
224+ appViewModel. directoryPickerLauncher = directoryPickerLauncher
223225
224226 setContent {
227+ var showDialog by remember { mutableStateOf(false ) }
228+
229+ LaunchedEffect (Unit ) { appViewModel.triggerDirectoryPicker.collect { showDialog = true } }
230+
231+ if (showDialog) {
232+ AppTheme {
233+ AlertDialog (
234+ onDismissRequest = {
235+ showDialog = false
236+ appViewModel.directoryPickerLauncher?.launch(null )
237+ },
238+ title = {
239+ Text (text = stringResource(id = R .string.taildrop_directory_picker_title))
240+ },
241+ text = { TaildropDirectoryPickerPrompt () },
242+ confirmButton = {
243+ PrimaryActionButton (
244+ onClick = {
245+ showDialog = false
246+ appViewModel.directoryPickerLauncher?.launch(null )
247+ }) {
248+ Text (text = stringResource(id = R .string.taildrop_directory_picker_button))
249+ }
250+ })
251+ }
252+ }
253+
225254 navController = rememberNavController()
226255
227256 AppTheme {
@@ -257,7 +286,6 @@ class MainActivity : ComponentActivity() {
257286 fun backTo (route : String ): () -> Unit = {
258287 navController.popBackStack(route = route, inclusive = false )
259288 }
260-
261289 val mainViewNav =
262290 MainViewNavigation (
263291 onNavigateToSettings = { navController.navigate(" settings" ) },
@@ -270,7 +298,6 @@ class MainActivity : ComponentActivity() {
270298 viewModel.enableSearchAutoFocus()
271299 navController.navigate(" search" )
272300 })
273-
274301 val settingsNav =
275302 SettingsNav (
276303 onNavigateToBugReport = { navController.navigate(" bugReport" ) },
@@ -285,7 +312,6 @@ class MainActivity : ComponentActivity() {
285312 onNavigateToPermissions = { navController.navigate(" permissions" ) },
286313 onBackToSettings = backTo(" settings" ),
287314 onNavigateBackHome = backTo(" main" ))
288-
289315 val exitNodePickerNav =
290316 ExitNodePickerNav (
291317 onNavigateBackHome = {
@@ -297,7 +323,6 @@ class MainActivity : ComponentActivity() {
297323 onNavigateBackToMullvad = backTo(" mullvad" ),
298324 onNavigateToMullvadCountry = { navController.navigate(" mullvad/$it " ) },
299325 onNavigateToRunAsExitNode = { navController.navigate(" runExitNode" ) })
300-
301326 val userSwitcherNav =
302327 UserSwitcherNav (
303328 backToSettings = backTo(" settings" ),
@@ -308,7 +333,11 @@ class MainActivity : ComponentActivity() {
308333 onNavigateToAuthKey = { navController.navigate(" loginWithAuthKey" ) })
309334
310335 composable(" main" , enterTransition = { fadeIn(animationSpec = tween(150 )) }) {
311- MainView (loginAtUrl = ::login, navigation = mainViewNav, viewModel = viewModel)
336+ MainView (
337+ loginAtUrl = ::login,
338+ navigation = mainViewNav,
339+ viewModel = viewModel,
340+ appViewModel = appViewModel)
312341 }
313342 composable(" search" ) {
314343 val autoFocus = viewModel.autoFocusSearch
@@ -318,7 +347,9 @@ class MainActivity : ComponentActivity() {
318347 onNavigateBack = { navController.popBackStack() },
319348 autoFocus = autoFocus)
320349 }
321- composable(" settings" ) { SettingsView (settingsNav) }
350+ composable(" settings" ) {
351+ SettingsView (settingsNav = settingsNav, appViewModel = appViewModel)
352+ }
322353 composable(" exitNodes" ) { ExitNodePicker (exitNodePickerNav) }
323354 composable(" health" ) { HealthView (backTo(" main" )) }
324355 composable(" mullvad" ) { MullvadExitNodePickerList (exitNodePickerNav) }
@@ -378,7 +409,6 @@ class MainActivity : ComponentActivity() {
378409 }
379410 }
380411 }
381-
382412 // Login actions are app wide. If we are told about a browse-to-url, we should render it
383413 // over whatever screen we happen to be on.
384414 loginQRCode.collectAsState().value?.let {
@@ -401,7 +431,6 @@ class MainActivity : ComponentActivity() {
401431 }
402432 }
403433 }
404-
405434 // Once we see a loginFinished event, clear the QR code which will dismiss the QR dialog.
406435 lifecycleScope.launch { Notifier .loginFinished.collect { _ -> loginQRCode.set(null ) } }
407436 }
@@ -422,7 +451,6 @@ class MainActivity : ComponentActivity() {
422451 fun isAnotherVpnActive (context : Context ): Boolean {
423452 val connectivityManager =
424453 context.getSystemService(Context .CONNECTIVITY_SERVICE ) as ConnectivityManager
425-
426454 val activeNetwork = connectivityManager.activeNetwork
427455 if (activeNetwork != null ) {
428456 val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
@@ -433,7 +461,6 @@ class MainActivity : ComponentActivity() {
433461 }
434462 return false
435463 }
436-
437464 // Returns true if we should render a QR code instead of launching a browser
438465 // for login requests
439466 private fun useQRCodeLogin (): Boolean {
@@ -449,7 +476,6 @@ class MainActivity : ComponentActivity() {
449476 if (this ::navController.isInitialized) {
450477 val previousEntry = navController.previousBackStackEntry
451478 TSLog .d(" MainActivity" , " onNewIntent: previousBackStackEntry = $previousEntry " )
452-
453479 if (previousEntry != null ) {
454480 navController.popBackStack(route = " main" , inclusive = false )
455481 } else {
@@ -478,7 +504,6 @@ class MainActivity : ComponentActivity() {
478504 putExtra(START_AT_ROOT , true )
479505 }
480506 startActivity(intent)
481-
482507 // Cancel coroutine once we've logged in
483508 this @launch.cancel()
484509 }
@@ -487,7 +512,6 @@ class MainActivity : ComponentActivity() {
487512 TSLog .e(TAG , " Login: failed to start MainActivity: $e " )
488513 }
489514 }
490-
491515 val url = urlString.toUri()
492516 try {
493517 val customTabsIntent = CustomTabsIntent .Builder ().build()
0 commit comments