Skip to content

Commit b48b2b1

Browse files
barnstarkari-ts
andauthored
android: defer taildrop selector until first taildrop attempt (#684) (#686)
Move Taildrop directory selector out of onboarding -Listen for Taildrop, and show selector if a directory has not been set Remove LocalBackend re-initialization -This is no longer necessary since the directory is set in FileOps Updates tailscale/corp#29211 (cherry picked from commit e68e640) Signed-off-by: kari-ts <[email protected]> Co-authored-by: kari-ts <[email protected]>
1 parent 7071d41 commit b48b2b1

File tree

12 files changed

+284
-350
lines changed

12 files changed

+284
-350
lines changed

android/src/main/java/com/tailscale/ipn/App.kt

Lines changed: 16 additions & 79 deletions
Large diffs are not rendered by default.

android/src/main/java/com/tailscale/ipn/MainActivity.kt

Lines changed: 58 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,18 @@ import androidx.compose.animation.fadeIn
3434
import androidx.compose.animation.fadeOut
3535
import androidx.compose.animation.slideInHorizontally
3636
import androidx.compose.animation.slideOutHorizontally
37+
import androidx.compose.material3.AlertDialog
3738
import androidx.compose.material3.MaterialTheme
3839
import androidx.compose.material3.Surface
40+
import androidx.compose.material3.Text
41+
import androidx.compose.runtime.LaunchedEffect
3942
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
4047
import androidx.compose.ui.Modifier
48+
import androidx.compose.ui.res.stringResource
4149
import androidx.core.net.toUri
4250
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
4351
import androidx.lifecycle.ViewModelProvider
@@ -75,39 +83,38 @@ import com.tailscale.ipn.ui.view.MullvadInfoView
7583
import com.tailscale.ipn.ui.view.NotificationsView
7684
import com.tailscale.ipn.ui.view.PeerDetails
7785
import com.tailscale.ipn.ui.view.PermissionsView
86+
import com.tailscale.ipn.ui.view.PrimaryActionButton
7887
import com.tailscale.ipn.ui.view.RunExitNodeView
7988
import com.tailscale.ipn.ui.view.SearchView
8089
import com.tailscale.ipn.ui.view.SettingsView
8190
import com.tailscale.ipn.ui.view.SplitTunnelAppPickerView
8291
import com.tailscale.ipn.ui.view.SubnetRoutingView
8392
import com.tailscale.ipn.ui.view.TaildropDirView
93+
import com.tailscale.ipn.ui.view.TaildropDirectoryPickerPrompt
8494
import com.tailscale.ipn.ui.view.TailnetLockSetupView
8595
import com.tailscale.ipn.ui.view.UserSwitcherNav
8696
import com.tailscale.ipn.ui.view.UserSwitcherView
97+
import com.tailscale.ipn.ui.viewModel.AppViewModel
8798
import com.tailscale.ipn.ui.viewModel.ExitNodePickerNav
8899
import com.tailscale.ipn.ui.viewModel.MainViewModel
89100
import com.tailscale.ipn.ui.viewModel.MainViewModelFactory
90101
import com.tailscale.ipn.ui.viewModel.PermissionsViewModel
91102
import com.tailscale.ipn.ui.viewModel.PingViewModel
92103
import com.tailscale.ipn.ui.viewModel.SettingsNav
93-
import com.tailscale.ipn.ui.viewModel.VpnViewModel
104+
import com.tailscale.ipn.util.ShareFileHelper
94105
import com.tailscale.ipn.util.TSLog
95106
import kotlinx.coroutines.Dispatchers
96107
import kotlinx.coroutines.cancel
97108
import kotlinx.coroutines.flow.MutableStateFlow
98109
import kotlinx.coroutines.flow.StateFlow
99110
import kotlinx.coroutines.launch
100-
import libtailscale.Libtailscale
101111

102112
class 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()

android/src/main/java/com/tailscale/ipn/TaildropDirectoryStore.kt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,6 @@ object TaildropDirectoryStore {
1616
fun saveFileDirectory(directoryUri: Uri) {
1717
val prefs = App.get().getEncryptedPrefs()
1818
prefs.edit().putString(PREF_KEY_SAF_URI, directoryUri.toString()).commit()
19-
try {
20-
// Must restart Tailscale because a new LocalBackend with the new directory must be created.
21-
App.get().startLibtailscale(directoryUri.toString())
22-
} catch (e: Exception) {
23-
TSLog.d(
24-
"TaildropDirectoryStore",
25-
"saveFileDirectory: Failed to restart Libtailscale with the new directory: $e")
26-
}
2719
}
2820

2921
@Throws(IOException::class, GeneralSecurityException::class)

0 commit comments

Comments
 (0)