Skip to content

Commit 136d472

Browse files
committed
android: move taildrop directory selector out of onboarding
-ShareFileHelper manages directory readiness; when a file is being shared to the device, it emits a signal to prompt the user to pick a directory -Remove MDM auth key check; there is no longer any need to make assumptions about Taildrop usage, and we only show the directory selector when they are receiving a Taildropped file -Listen for Taildrop receipt in application view model (formerly VpnViewModel, now renamed due to its expanded scope), since Taildrop can occur even without MainActivity -Switch from StateFlow to SharedFlow since this is an event that only needs to be handled once rather than a persistent UI state. -ShareFileHelper keeps track of Taildrop dir rather than the Taildrop extension managerOptions; this allows the correct directory to be used without having to send a new request or restart LocalBackend -Don't restart LocalBackend on Taildrop dir selection because this is no longer necessary Follow-up: implement resume Taildrop in SAF Updates tailscale/corp#29211 Signed-off-by: kari-ts <[email protected]>
1 parent 28f1931 commit 136d472

File tree

15 files changed

+299
-221
lines changed

15 files changed

+299
-221
lines changed

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ import com.tailscale.ipn.ui.model.Ipn
3333
import com.tailscale.ipn.ui.model.Netmap
3434
import com.tailscale.ipn.ui.notifier.HealthNotifier
3535
import com.tailscale.ipn.ui.notifier.Notifier
36-
import com.tailscale.ipn.ui.viewModel.VpnViewModel
37-
import com.tailscale.ipn.ui.viewModel.VpnViewModelFactory
36+
import com.tailscale.ipn.ui.viewModel.AppViewModel
37+
import com.tailscale.ipn.ui.viewModel.AppViewModelFactory
3838
import com.tailscale.ipn.util.FeatureFlags
3939
import com.tailscale.ipn.util.ShareFileHelper
4040
import com.tailscale.ipn.util.TSLog
@@ -211,23 +211,25 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
211211
* Tailscale because directFileRoot must be set before LocalBackend starts being used.
212212
*/
213213
fun startLibtailscale(directFileRoot: String) {
214-
ShareFileHelper.init(this, directFileRoot)
215214
app = Libtailscale.start(this.filesDir.absolutePath, directFileRoot, this)
215+
ShareFileHelper.init(this, app, directFileRoot, applicationScope)
216216
Request.setApp(app)
217217
Notifier.setApp(app)
218218
Notifier.start(applicationScope)
219219
}
220220

221221
private fun initViewModels() {
222-
vpnViewModel = ViewModelProvider(this, VpnViewModelFactory(this)).get(VpnViewModel::class.java)
222+
appViewModel =
223+
ViewModelProvider(this, AppViewModelFactory(this, ShareFileHelper.observeTaildropPrompt()))
224+
.get(AppViewModel::class.java)
223225
}
224226

225227
fun setWantRunning(wantRunning: Boolean, onSuccess: (() -> Unit)? = null) {
226228
val callback: (Result<Ipn.Prefs>) -> Unit = { result ->
227229
result.fold(
228230
onSuccess = { onSuccess?.invoke() },
229231
onFailure = { error ->
230-
TSLog.d("TAG", "Set want running: failed to update preferences: ${error.message}")
232+
TSLog.d(TAG, "Set want running: failed to update preferences: ${error.message}")
231233
})
232234
}
233235
Client(applicationScope)
@@ -390,7 +392,7 @@ open class UninitializedApp : Application() {
390392
private lateinit var appInstance: UninitializedApp
391393
lateinit var notificationManager: NotificationManagerCompat
392394

393-
lateinit var vpnViewModel: VpnViewModel
395+
lateinit var appViewModel: AppViewModel
394396

395397
@JvmStatic
396398
fun get(): UninitializedApp {
@@ -577,8 +579,8 @@ open class UninitializedApp : Application() {
577579
return builtInDisallowedPackageNames + userDisallowed
578580
}
579581

580-
fun getAppScopedViewModel(): VpnViewModel {
581-
return vpnViewModel
582+
fun getAppScopedViewModel(): AppViewModel {
583+
return appViewModel
582584
}
583585

584586
val builtInDisallowedPackageNames: List<String> =

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

Lines changed: 55 additions & 11 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,41 @@ 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>
105115
private val viewModel: MainViewModel by lazy {
106116
val app = App.get()
107-
vpnViewModel = app.getAppScopedViewModel()
108-
ViewModelProvider(this, MainViewModelFactory(vpnViewModel)).get(MainViewModel::class.java)
117+
appViewModel = app.getAppScopedViewModel()
118+
ViewModelProvider(this, MainViewModelFactory(appViewModel)).get(MainViewModel::class.java)
109119
}
110-
private lateinit var vpnViewModel: VpnViewModel
120+
private lateinit var appViewModel: AppViewModel
111121
val permissionsViewModel: PermissionsViewModel by viewModels()
112122

113123
companion object {
@@ -132,7 +142,7 @@ class MainActivity : ComponentActivity() {
132142

133143
// grab app to make sure it initializes
134144
App.get()
135-
vpnViewModel = ViewModelProvider(App.get()).get(VpnViewModel::class.java)
145+
appViewModel = ViewModelProvider(App.get()).get(AppViewModel::class.java)
136146

137147
val rm = getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager
138148
MDMSettings.update(App.get(), rm)
@@ -154,15 +164,15 @@ class MainActivity : ComponentActivity() {
154164
registerForActivityResult(VpnPermissionContract()) { granted ->
155165
if (granted) {
156166
TSLog.d("VpnPermission", "VPN permission granted")
157-
vpnViewModel.setVpnPrepared(true)
167+
appViewModel.setVpnPrepared(true)
158168
App.get().startVPN()
159169
} else {
160170
if (isAnotherVpnActive(this)) {
161171
TSLog.d("VpnPermission", "Another VPN is likely active")
162172
showOtherVPNConflictDialog()
163173
} else {
164174
TSLog.d("VpnPermission", "Permission was denied by the user")
165-
vpnViewModel.setVpnPrepared(false)
175+
appViewModel.setVpnPrepared(false)
166176

167177
AlertDialog.Builder(this)
168178
.setTitle(R.string.vpn_permission_needed)
@@ -198,9 +208,10 @@ class MainActivity : ComponentActivity() {
198208

199209
lifecycleScope.launch(Dispatchers.IO) {
200210
try {
201-
Libtailscale.setDirectFileRoot(uri.toString())
202211
TaildropDirectoryStore.saveFileDirectory(uri)
203212
permissionsViewModel.refreshCurrentDir()
213+
ShareFileHelper.notifyDirectoryReady()
214+
ShareFileHelper.setUri(uri.toString())
204215
} catch (e: Exception) {
205216
TSLog.e("MainActivity", "Failed to set Taildrop root: $e")
206217
}
@@ -219,9 +230,38 @@ class MainActivity : ComponentActivity() {
219230
}
220231
}
221232

222-
viewModel.setDirectoryPickerLauncher(directoryPickerLauncher)
233+
appViewModel.directoryPickerLauncher = directoryPickerLauncher
223234

224235
setContent {
236+
var showDialog by remember { mutableStateOf(false) }
237+
238+
LaunchedEffect(Unit) {
239+
appViewModel.showDirectoryPickerInterstitial.collect { showDialog = true }
240+
}
241+
242+
if (showDialog) {
243+
AppTheme {
244+
AlertDialog(
245+
onDismissRequest = {
246+
showDialog = false
247+
appViewModel.directoryPickerLauncher?.launch(null)
248+
},
249+
title = {
250+
Text(text = stringResource(id = R.string.taildrop_directory_picker_title))
251+
},
252+
text = { TaildropDirectoryPickerPrompt() },
253+
confirmButton = {
254+
PrimaryActionButton(
255+
onClick = {
256+
showDialog = false
257+
appViewModel.directoryPickerLauncher?.launch(null)
258+
}) {
259+
Text(text = stringResource(id = R.string.taildrop_directory_picker_button))
260+
}
261+
})
262+
}
263+
}
264+
225265
navController = rememberNavController()
226266

227267
AppTheme {
@@ -308,7 +348,11 @@ class MainActivity : ComponentActivity() {
308348
onNavigateToAuthKey = { navController.navigate("loginWithAuthKey") })
309349

310350
composable("main", enterTransition = { fadeIn(animationSpec = tween(150)) }) {
311-
MainView(loginAtUrl = ::login, navigation = mainViewNav, viewModel = viewModel)
351+
MainView(
352+
loginAtUrl = ::login,
353+
navigation = mainViewNav,
354+
viewModel = viewModel,
355+
appViewModel = appViewModel)
312356
}
313357
composable("search") {
314358
val autoFocus = viewModel.autoFocusSearch

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ 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 {
19+
/* try {
2020
// Must restart Tailscale because a new LocalBackend with the new directory must be created.
2121
App.get().startLibtailscale(directoryUri.toString())
2222
} catch (e: Exception) {
2323
TSLog.d(
2424
"TaildropDirectoryStore",
2525
"saveFileDirectory: Failed to restart Libtailscale with the new directory: $e")
26-
}
26+
}*/
2727
}
2828

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

android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,11 @@ import com.tailscale.ipn.ui.util.LoadingIndicator
109109
import com.tailscale.ipn.ui.util.PeerSet
110110
import com.tailscale.ipn.ui.util.itemsWithDividers
111111
import com.tailscale.ipn.ui.util.set
112+
import com.tailscale.ipn.ui.viewModel.AppViewModel
112113
import com.tailscale.ipn.ui.viewModel.IpnViewModel.NodeState
113114
import com.tailscale.ipn.ui.viewModel.MainViewModel
114-
import com.tailscale.ipn.ui.viewModel.VpnViewModel
115115
import com.tailscale.ipn.util.FeatureFlags
116+
import com.tailscale.ipn.util.ShareFileHelper
116117

117118
// Navigation actions for the MainView
118119
data class MainViewNavigation(
@@ -129,6 +130,7 @@ fun MainView(
129130
loginAtUrl: (String) -> Unit,
130131
navigation: MainViewNavigation,
131132
viewModel: MainViewModel,
133+
appViewModel: AppViewModel
132134
) {
133135
val currentPingDevice by viewModel.pingViewModel.peer.collectAsState()
134136
val healthIcon by viewModel.healthIcon.collectAsState()
@@ -152,7 +154,7 @@ fun MainView(
152154
val disableToggle by MDMSettings.forceEnabled.flow.collectAsState()
153155
val showKeyExpiry by viewModel.showExpiry.collectAsState(initial = false)
154156
val showDirectoryPickerInterstitial by
155-
viewModel.showDirectoryPickerInterstitial.collectAsState()
157+
appViewModel.showDirectoryPickerInterstitial.collectAsState()
156158

157159
// Hide the header only on Android TV when the user needs to login
158160
val hideHeader = (isAndroidTV() && state == Ipn.State.NeedsLogin)
@@ -219,14 +221,6 @@ fun MainView(
219221
LaunchVpnPermissionIfNeeded(viewModel)
220222
PromptForMissingPermissions(viewModel)
221223

222-
if (!viewModel.skipPromptsForAuthKeyLogin()) {
223-
LaunchedEffect(state) {
224-
if (state == Ipn.State.Running && !isAndroidTV()) {
225-
viewModel.checkIfTaildropDirectorySelected()
226-
}
227-
}
228-
}
229-
230224
if (showKeyExpiry) {
231225
ExpiryNotification(netmap = netmap, action = { viewModel.login() })
232226
}
@@ -259,25 +253,6 @@ fun MainView(
259253
{ viewModel.showVPNPermissionLauncherIfUnauthorized() })
260254
}
261255
}
262-
263-
showDirectoryPickerInterstitial.let { show ->
264-
if (show) {
265-
AppTheme {
266-
AlertDialog(
267-
onDismissRequest = { viewModel.showDirectoryPickerLauncher() },
268-
title = {
269-
Text(text = stringResource(id = R.string.taildrop_directory_picker_title))
270-
},
271-
text = { TaildropDirectoryPickerPrompt() },
272-
confirmButton = {
273-
PrimaryActionButton(onClick = { viewModel.showDirectoryPickerLauncher() }) {
274-
Text(
275-
text = stringResource(id = R.string.taildrop_directory_picker_button))
276-
}
277-
})
278-
}
279-
}
280-
}
281256
}
282257
currentPingDevice?.let { _ ->
283258
ModalBottomSheet(onDismissRequest = { viewModel.onPingDismissal() }) {
@@ -869,8 +844,8 @@ fun Search(
869844
@Preview
870845
@Composable
871846
fun MainViewPreview() {
872-
val vpnViewModel = VpnViewModel(App.get())
873-
val vm = MainViewModel(vpnViewModel)
847+
val appViewModel = AppViewModel(App.get(), ShareFileHelper.taildropPrompt)
848+
val vm = MainViewModel(appViewModel)
874849

875850
MainView(
876851
{},
@@ -880,5 +855,6 @@ fun MainViewPreview() {
880855
onNavigateToExitNodes = {},
881856
onNavigateToHealth = {},
882857
onNavigateToSearch = {}),
883-
vm)
858+
vm,
859+
appViewModel)
884860
}

android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@ import com.tailscale.ipn.ui.util.Lists
4040
import com.tailscale.ipn.ui.util.set
4141
import com.tailscale.ipn.ui.viewModel.SettingsNav
4242
import com.tailscale.ipn.ui.viewModel.SettingsViewModel
43-
import com.tailscale.ipn.ui.viewModel.VpnViewModel
43+
import com.tailscale.ipn.ui.viewModel.AppViewModel
4444

4545
@Composable
4646
fun SettingsView(
4747
settingsNav: SettingsNav,
4848
viewModel: SettingsViewModel = viewModel(),
49-
vpnViewModel: VpnViewModel = viewModel()
49+
appViewModel: AppViewModel = viewModel()
5050
) {
5151
val handler = LocalUriHandler.current
5252

@@ -55,7 +55,7 @@ fun SettingsView(
5555
val managedByOrganization by viewModel.managedByOrganization.collectAsState()
5656
val tailnetLockEnabled by viewModel.tailNetLockEnabled.collectAsState()
5757
val corpDNSEnabled by viewModel.corpDNSEnabled.collectAsState()
58-
val isVPNPrepared by vpnViewModel.vpnPrepared.collectAsState()
58+
val isVPNPrepared by appViewModel.vpnPrepared.collectAsState()
5959
val showTailnetLock by MDMSettings.manageTailnetLock.flow.collectAsState()
6060
val useTailscaleSubnets by MDMSettings.useTailscaleSubnets.flow.collectAsState()
6161

0 commit comments

Comments
 (0)