@@ -34,10 +34,18 @@ import androidx.compose.animation.fadeIn
34
34
import androidx.compose.animation.fadeOut
35
35
import androidx.compose.animation.slideInHorizontally
36
36
import androidx.compose.animation.slideOutHorizontally
37
+ import androidx.compose.material3.AlertDialog
37
38
import androidx.compose.material3.MaterialTheme
38
39
import androidx.compose.material3.Surface
40
+ import androidx.compose.material3.Text
41
+ import androidx.compose.runtime.LaunchedEffect
39
42
import 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
40
47
import androidx.compose.ui.Modifier
48
+ import androidx.compose.ui.res.stringResource
41
49
import androidx.core.net.toUri
42
50
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
43
51
import androidx.lifecycle.ViewModelProvider
@@ -75,39 +83,42 @@ import com.tailscale.ipn.ui.view.MullvadInfoView
75
83
import com.tailscale.ipn.ui.view.NotificationsView
76
84
import com.tailscale.ipn.ui.view.PeerDetails
77
85
import com.tailscale.ipn.ui.view.PermissionsView
86
+ import com.tailscale.ipn.ui.view.PrimaryActionButton
78
87
import com.tailscale.ipn.ui.view.RunExitNodeView
79
88
import com.tailscale.ipn.ui.view.SearchView
80
89
import com.tailscale.ipn.ui.view.SettingsView
81
90
import com.tailscale.ipn.ui.view.SplitTunnelAppPickerView
82
91
import com.tailscale.ipn.ui.view.SubnetRoutingView
83
92
import com.tailscale.ipn.ui.view.TaildropDirView
93
+ import com.tailscale.ipn.ui.view.TaildropDirectoryPickerPrompt
84
94
import com.tailscale.ipn.ui.view.TailnetLockSetupView
85
95
import com.tailscale.ipn.ui.view.UserSwitcherNav
86
96
import com.tailscale.ipn.ui.view.UserSwitcherView
97
+ import com.tailscale.ipn.ui.viewModel.AppViewModel
98
+ import com.tailscale.ipn.ui.viewModel.AppViewModelFactory
87
99
import com.tailscale.ipn.ui.viewModel.ExitNodePickerNav
88
100
import com.tailscale.ipn.ui.viewModel.MainViewModel
89
101
import com.tailscale.ipn.ui.viewModel.MainViewModelFactory
90
102
import com.tailscale.ipn.ui.viewModel.PermissionsViewModel
91
103
import com.tailscale.ipn.ui.viewModel.PingViewModel
92
104
import com.tailscale.ipn.ui.viewModel.SettingsNav
93
- import com.tailscale.ipn.ui.viewModel.VpnViewModel
105
+ import com.tailscale.ipn.util.ShareFileHelper
94
106
import com.tailscale.ipn.util.TSLog
95
107
import kotlinx.coroutines.Dispatchers
96
108
import kotlinx.coroutines.cancel
97
109
import kotlinx.coroutines.flow.MutableStateFlow
98
110
import kotlinx.coroutines.flow.StateFlow
99
111
import kotlinx.coroutines.launch
100
- import libtailscale.Libtailscale
101
112
102
113
class MainActivity : ComponentActivity () {
103
114
private lateinit var navController: NavHostController
104
115
private lateinit var vpnPermissionLauncher: ActivityResultLauncher <Intent >
105
116
private val viewModel: MainViewModel by lazy {
106
117
val app = App .get()
107
- vpnViewModel = app.getAppScopedViewModel()
108
- ViewModelProvider (this , MainViewModelFactory (vpnViewModel )).get(MainViewModel ::class .java)
118
+ appViewModel = app.getAppScopedViewModel()
119
+ ViewModelProvider (this , MainViewModelFactory (appViewModel )).get(MainViewModel ::class .java)
109
120
}
110
- private lateinit var vpnViewModel : VpnViewModel
121
+ private lateinit var appViewModel : AppViewModel
111
122
val permissionsViewModel: PermissionsViewModel by viewModels()
112
123
113
124
companion object {
@@ -132,7 +143,11 @@ class MainActivity : ComponentActivity() {
132
143
133
144
// grab app to make sure it initializes
134
145
App .get()
135
- vpnViewModel = ViewModelProvider (App .get()).get(VpnViewModel ::class .java)
146
+ val appVmFactory =
147
+ AppViewModelFactory (
148
+ application = this .application, taildropPrompt = ShareFileHelper .taildropPrompt)
149
+
150
+ appViewModel = ViewModelProvider (this , appVmFactory).get(AppViewModel ::class .java)
136
151
137
152
val rm = getSystemService(Context .RESTRICTIONS_SERVICE ) as RestrictionsManager
138
153
MDMSettings .update(App .get(), rm)
@@ -154,15 +169,15 @@ class MainActivity : ComponentActivity() {
154
169
registerForActivityResult(VpnPermissionContract ()) { granted ->
155
170
if (granted) {
156
171
TSLog .d(" VpnPermission" , " VPN permission granted" )
157
- vpnViewModel .setVpnPrepared(true )
172
+ appViewModel .setVpnPrepared(true )
158
173
App .get().startVPN()
159
174
} else {
160
175
if (isAnotherVpnActive(this )) {
161
176
TSLog .d(" VpnPermission" , " Another VPN is likely active" )
162
177
showOtherVPNConflictDialog()
163
178
} else {
164
179
TSLog .d(" VpnPermission" , " Permission was denied by the user" )
165
- vpnViewModel .setVpnPrepared(false )
180
+ appViewModel .setVpnPrepared(false )
166
181
167
182
AlertDialog .Builder (this )
168
183
.setTitle(R .string.vpn_permission_needed)
@@ -198,9 +213,10 @@ class MainActivity : ComponentActivity() {
198
213
199
214
lifecycleScope.launch(Dispatchers .IO ) {
200
215
try {
201
- Libtailscale .setDirectFileRoot(uri.toString())
202
216
TaildropDirectoryStore .saveFileDirectory(uri)
203
217
permissionsViewModel.refreshCurrentDir()
218
+ ShareFileHelper .notifyDirectoryReady()
219
+ ShareFileHelper .setUri(uri.toString())
204
220
} catch (e: Exception ) {
205
221
TSLog .e(" MainActivity" , " Failed to set Taildrop root: $e " )
206
222
}
@@ -219,9 +235,38 @@ class MainActivity : ComponentActivity() {
219
235
}
220
236
}
221
237
222
- viewModel.setDirectoryPickerLauncher( directoryPickerLauncher)
238
+ appViewModel. directoryPickerLauncher = directoryPickerLauncher
223
239
224
240
setContent {
241
+ var showDialog by remember { mutableStateOf(false ) }
242
+
243
+ LaunchedEffect (Unit ) {
244
+ appViewModel.showDirectoryPickerInterstitial.collect { showDialog = true }
245
+ }
246
+
247
+ if (showDialog) {
248
+ AppTheme {
249
+ AlertDialog (
250
+ onDismissRequest = {
251
+ showDialog = false
252
+ appViewModel.directoryPickerLauncher?.launch(null )
253
+ },
254
+ title = {
255
+ Text (text = stringResource(id = R .string.taildrop_directory_picker_title))
256
+ },
257
+ text = { TaildropDirectoryPickerPrompt () },
258
+ confirmButton = {
259
+ PrimaryActionButton (
260
+ onClick = {
261
+ showDialog = false
262
+ appViewModel.directoryPickerLauncher?.launch(null )
263
+ }) {
264
+ Text (text = stringResource(id = R .string.taildrop_directory_picker_button))
265
+ }
266
+ })
267
+ }
268
+ }
269
+
225
270
navController = rememberNavController()
226
271
227
272
AppTheme {
@@ -308,7 +353,11 @@ class MainActivity : ComponentActivity() {
308
353
onNavigateToAuthKey = { navController.navigate(" loginWithAuthKey" ) })
309
354
310
355
composable(" main" , enterTransition = { fadeIn(animationSpec = tween(150 )) }) {
311
- MainView (loginAtUrl = ::login, navigation = mainViewNav, viewModel = viewModel)
356
+ MainView (
357
+ loginAtUrl = ::login,
358
+ navigation = mainViewNav,
359
+ viewModel = viewModel,
360
+ appViewModel = appViewModel)
312
361
}
313
362
composable(" search" ) {
314
363
val autoFocus = viewModel.autoFocusSearch
@@ -318,7 +367,11 @@ class MainActivity : ComponentActivity() {
318
367
onNavigateBack = { navController.popBackStack() },
319
368
autoFocus = autoFocus)
320
369
}
321
- composable(" settings" ) { SettingsView (settingsNav) }
370
+ composable(" settings" ) {
371
+ SettingsView (
372
+ settingsNav = settingsNav, appViewModel = appViewModel
373
+ )
374
+ }
322
375
composable(" exitNodes" ) { ExitNodePicker (exitNodePickerNav) }
323
376
composable(" health" ) { HealthView (backTo(" main" )) }
324
377
composable(" mullvad" ) { MullvadExitNodePickerList (exitNodePickerNav) }
0 commit comments