Skip to content

Commit da2e5c3

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 TODO: in OSS, SAF mode should be determined by OS, and not by the dir, since the dir may not be set immediately. Updates tailscale/corp#29211 Signed-off-by: kari-ts <[email protected]>
1 parent 28f1931 commit da2e5c3

File tree

10 files changed

+255
-173
lines changed

10 files changed

+255
-173
lines changed

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

Lines changed: 11 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,26 @@ 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)
214+
ShareFileHelper.init(this, directFileRoot, applicationScope)
215215
app = Libtailscale.start(this.filesDir.absolutePath, directFileRoot, this)
216216
Request.setApp(app)
217217
Notifier.setApp(app)
218218
Notifier.start(applicationScope)
219+
TSLog.d(TAG, "Starting libtailscale with directFileRoot: $directFileRoot")
219220
}
220221

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

225228
fun setWantRunning(wantRunning: Boolean, onSuccess: (() -> Unit)? = null) {
226229
val callback: (Result<Ipn.Prefs>) -> Unit = { result ->
227230
result.fold(
228231
onSuccess = { onSuccess?.invoke() },
229232
onFailure = { error ->
230-
TSLog.d("TAG", "Set want running: failed to update preferences: ${error.message}")
233+
TSLog.d(TAG, "Set want running: failed to update preferences: ${error.message}")
231234
})
232235
}
233236
Client(applicationScope)
@@ -390,7 +393,7 @@ open class UninitializedApp : Application() {
390393
private lateinit var appInstance: UninitializedApp
391394
lateinit var notificationManager: NotificationManagerCompat
392395

393-
lateinit var vpnViewModel: VpnViewModel
396+
lateinit var appViewModel: AppViewModel
394397

395398
@JvmStatic
396399
fun get(): UninitializedApp {
@@ -577,8 +580,8 @@ open class UninitializedApp : Application() {
577580
return builtInDisallowedPackageNames + userDisallowed
578581
}
579582

580-
fun getAppScopedViewModel(): VpnViewModel {
581-
return vpnViewModel
583+
fun getAppScopedViewModel(): AppViewModel {
584+
return appViewModel
582585
}
583586

584587
val builtInDisallowedPackageNames: List<String> =

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

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,14 @@ import com.tailscale.ipn.ui.view.TaildropDirView
8484
import com.tailscale.ipn.ui.view.TailnetLockSetupView
8585
import com.tailscale.ipn.ui.view.UserSwitcherNav
8686
import com.tailscale.ipn.ui.view.UserSwitcherView
87+
import com.tailscale.ipn.ui.viewModel.AppViewModel
8788
import com.tailscale.ipn.ui.viewModel.ExitNodePickerNav
8889
import com.tailscale.ipn.ui.viewModel.MainViewModel
8990
import com.tailscale.ipn.ui.viewModel.MainViewModelFactory
9091
import com.tailscale.ipn.ui.viewModel.PermissionsViewModel
9192
import com.tailscale.ipn.ui.viewModel.PingViewModel
9293
import com.tailscale.ipn.ui.viewModel.SettingsNav
93-
import com.tailscale.ipn.ui.viewModel.VpnViewModel
94+
import com.tailscale.ipn.util.ShareFileHelper
9495
import com.tailscale.ipn.util.TSLog
9596
import kotlinx.coroutines.Dispatchers
9697
import kotlinx.coroutines.cancel
@@ -104,10 +105,10 @@ class MainActivity : ComponentActivity() {
104105
private lateinit var vpnPermissionLauncher: ActivityResultLauncher<Intent>
105106
private val viewModel: MainViewModel by lazy {
106107
val app = App.get()
107-
vpnViewModel = app.getAppScopedViewModel()
108-
ViewModelProvider(this, MainViewModelFactory(vpnViewModel)).get(MainViewModel::class.java)
108+
appViewModel = app.getAppScopedViewModel()
109+
ViewModelProvider(this, MainViewModelFactory(appViewModel)).get(MainViewModel::class.java)
109110
}
110-
private lateinit var vpnViewModel: VpnViewModel
111+
private lateinit var appViewModel: AppViewModel
111112
val permissionsViewModel: PermissionsViewModel by viewModels()
112113

113114
companion object {
@@ -132,7 +133,7 @@ class MainActivity : ComponentActivity() {
132133

133134
// grab app to make sure it initializes
134135
App.get()
135-
vpnViewModel = ViewModelProvider(App.get()).get(VpnViewModel::class.java)
136+
appViewModel = ViewModelProvider(App.get()).get(AppViewModel::class.java)
136137

137138
val rm = getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager
138139
MDMSettings.update(App.get(), rm)
@@ -154,15 +155,15 @@ class MainActivity : ComponentActivity() {
154155
registerForActivityResult(VpnPermissionContract()) { granted ->
155156
if (granted) {
156157
TSLog.d("VpnPermission", "VPN permission granted")
157-
vpnViewModel.setVpnPrepared(true)
158+
appViewModel.setVpnPrepared(true)
158159
App.get().startVPN()
159160
} else {
160161
if (isAnotherVpnActive(this)) {
161162
TSLog.d("VpnPermission", "Another VPN is likely active")
162163
showOtherVPNConflictDialog()
163164
} else {
164165
TSLog.d("VpnPermission", "Permission was denied by the user")
165-
vpnViewModel.setVpnPrepared(false)
166+
appViewModel.setVpnPrepared(false)
166167

167168
AlertDialog.Builder(this)
168169
.setTitle(R.string.vpn_permission_needed)
@@ -201,6 +202,7 @@ class MainActivity : ComponentActivity() {
201202
Libtailscale.setDirectFileRoot(uri.toString())
202203
TaildropDirectoryStore.saveFileDirectory(uri)
203204
permissionsViewModel.refreshCurrentDir()
205+
ShareFileHelper.notifyDirectoryReady()
204206
} catch (e: Exception) {
205207
TSLog.e("MainActivity", "Failed to set Taildrop root: $e")
206208
}
@@ -219,7 +221,7 @@ class MainActivity : ComponentActivity() {
219221
}
220222
}
221223

222-
viewModel.setDirectoryPickerLauncher(directoryPickerLauncher)
224+
appViewModel.setDirectoryPickerLauncher(directoryPickerLauncher)
223225

224226
setContent {
225227
navController = rememberNavController()
@@ -308,7 +310,11 @@ class MainActivity : ComponentActivity() {
308310
onNavigateToAuthKey = { navController.navigate("loginWithAuthKey") })
309311

310312
composable("main", enterTransition = { fadeIn(animationSpec = tween(150)) }) {
311-
MainView(loginAtUrl = ::login, navigation = mainViewNav, viewModel = viewModel)
313+
MainView(
314+
loginAtUrl = ::login,
315+
navigation = mainViewNav,
316+
viewModel = viewModel,
317+
appViewModel = appViewModel)
312318
}
313319
composable("search") {
314320
val autoFocus = viewModel.autoFocusSearch

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

Lines changed: 22 additions & 24 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,12 +130,13 @@ 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()
135137

136138
LoadingIndicator.Wrap {
137-
Scaffold(contentWindowInsets = WindowInsets.Companion.statusBars) { paddingInsets ->
139+
Scaffold(contentWindowInsets = WindowInsets.statusBars) { paddingInsets ->
138140
Column(
139141
modifier = Modifier.fillMaxWidth().padding(paddingInsets),
140142
verticalArrangement = Arrangement.Center) {
@@ -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)
@@ -217,15 +219,7 @@ fun MainView(
217219
Ipn.State.Running -> {
218220
viewModel.maybeRequestVpnPermission()
219221
LaunchVpnPermissionIfNeeded(viewModel)
220-
PromptForMissingPermissions(viewModel)
221-
222-
if (!viewModel.skipPromptsForAuthKeyLogin()) {
223-
LaunchedEffect(state) {
224-
if (state == Ipn.State.Running && !isAndroidTV()) {
225-
viewModel.checkIfTaildropDirectorySelected()
226-
}
227-
}
228-
}
222+
PromptForMissingPermissions(appViewModel)
229223

230224
if (showKeyExpiry) {
231225
ExpiryNotification(netmap = netmap, action = { viewModel.login() })
@@ -260,20 +254,23 @@ fun MainView(
260254
}
261255
}
262256

263-
showDirectoryPickerInterstitial.let { show ->
264-
if (show) {
257+
appViewModel.showDirectoryPickerInterstitial.let { show ->
258+
if (show.value) {
265259
AppTheme {
266260
AlertDialog(
267-
onDismissRequest = { viewModel.showDirectoryPickerLauncher() },
261+
onDismissRequest = { appViewModel.showDirectoryPickerLauncher() },
268262
title = {
269263
Text(text = stringResource(id = R.string.taildrop_directory_picker_title))
270264
},
271265
text = { TaildropDirectoryPickerPrompt() },
272266
confirmButton = {
273-
PrimaryActionButton(onClick = { viewModel.showDirectoryPickerLauncher() }) {
274-
Text(
275-
text = stringResource(id = R.string.taildrop_directory_picker_button))
276-
}
267+
PrimaryActionButton(
268+
onClick = { appViewModel.showDirectoryPickerLauncher() }) {
269+
Text(
270+
text =
271+
stringResource(
272+
id = R.string.taildrop_directory_picker_button))
273+
}
277274
})
278275
}
279276
}
@@ -797,8 +794,8 @@ fun ExpiryNotification(netmap: Netmap.NetworkMap?, action: () -> Unit = {}) {
797794

798795
@OptIn(ExperimentalPermissionsApi::class)
799796
@Composable
800-
fun PromptForMissingPermissions(viewModel: MainViewModel) {
801-
if (viewModel.skipPromptsForAuthKeyLogin()) {
797+
fun PromptForMissingPermissions(appViewModel: AppViewModel) {
798+
if (appViewModel.skipPromptsForAuthKeyLogin()) {
802799
return
803800
}
804801

@@ -869,8 +866,8 @@ fun Search(
869866
@Preview
870867
@Composable
871868
fun MainViewPreview() {
872-
val vpnViewModel = VpnViewModel(App.get())
873-
val vm = MainViewModel(vpnViewModel)
869+
val appViewModel = AppViewModel(App.get(), ShareFileHelper.taildropPrompt)
870+
val vm = MainViewModel(appViewModel)
874871

875872
MainView(
876873
{},
@@ -880,5 +877,6 @@ fun MainViewPreview() {
880877
onNavigateToExitNodes = {},
881878
onNavigateToHealth = {},
882879
onNavigateToSearch = {}),
883-
vm)
880+
vm,
881+
appViewModel)
884882
}

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)