android: defer taildrop selector until first taildrop attempt

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

Signed-off-by: kari-ts <kari@tailscale.com>
kari/dirselmove
kari-ts 4 months ago
parent c887c926da
commit 36a867d2d0

@ -48,6 +48,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
import androidx.navigation.NavType
@ -94,7 +95,6 @@ import com.tailscale.ipn.ui.view.TailnetLockSetupView
import com.tailscale.ipn.ui.view.UserSwitcherNav
import com.tailscale.ipn.ui.view.UserSwitcherView
import com.tailscale.ipn.ui.viewModel.AppViewModel
import com.tailscale.ipn.ui.viewModel.AppViewModelFactory
import com.tailscale.ipn.ui.viewModel.ExitNodePickerNav
import com.tailscale.ipn.ui.viewModel.MainViewModel
import com.tailscale.ipn.ui.viewModel.MainViewModelFactory
@ -112,12 +112,9 @@ import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
private lateinit var navController: NavHostController
private lateinit var vpnPermissionLauncher: ActivityResultLauncher<Intent>
private val appViewModel: AppViewModel by viewModels {
AppViewModelFactory(
application = this.application, taildropPrompt = ShareFileHelper.taildropPrompt)
}
private val viewModel: MainViewModel by viewModels { MainViewModelFactory(appViewModel) }
private lateinit var appViewModel: AppViewModel
private lateinit var viewModel: MainViewModel
val permissionsViewModel: PermissionsViewModel by viewModels()
@ -144,6 +141,11 @@ class MainActivity : ComponentActivity() {
// grab app to make sure it initializes
App.get()
appViewModel = (application as App).getAppScopedViewModel()
viewModel =
ViewModelProvider(this, MainViewModelFactory(appViewModel)).get(MainViewModel::class.java)
val rm = getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager
MDMSettings.update(App.get(), rm)
@ -232,6 +234,19 @@ class MainActivity : ComponentActivity() {
appViewModel.directoryPickerLauncher = directoryPickerLauncher
lifecycleScope.launchWhenStarted {
appViewModel.triggerDirectoryPicker.collect {
AlertDialog.Builder(this@MainActivity)
.setTitle(R.string.taildrop_directory_picker_title)
.setMessage(R.string.taildrop_directory_picker_body)
.setPositiveButton(R.string.taildrop_directory_picker_button) { _, _ ->
appViewModel.directoryPickerLauncher?.launch(null)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
}
setContent {
var showDialog by remember { mutableStateOf(false) }
@ -242,10 +257,7 @@ class MainActivity : ComponentActivity() {
if (showDialog) {
AppTheme {
AlertDialog(
onDismissRequest = {
showDialog = false
appViewModel.directoryPickerLauncher?.launch(null)
},
onDismissRequest = { showDialog = false },
title = {
Text(text = stringResource(id = R.string.taildrop_directory_picker_title))
},

@ -16,14 +16,6 @@ object TaildropDirectoryStore {
fun saveFileDirectory(directoryUri: Uri) {
val prefs = App.get().getEncryptedPrefs()
prefs.edit().putString(PREF_KEY_SAF_URI, directoryUri.toString()).commit()
try {
// Must restart Tailscale because a new LocalBackend with the new directory must be created.
App.get().startLibtailscale(directoryUri.toString())
} catch (e: Exception) {
TSLog.d(
"TaildropDirectoryStore",
"saveFileDirectory: Failed to restart Libtailscale with the new directory: $e")
}
}
@Throws(IOException::class, GeneralSecurityException::class)

@ -70,11 +70,13 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.tailscale.ipn.App
import com.tailscale.ipn.R
import com.tailscale.ipn.mdm.MDMSettings
import com.tailscale.ipn.mdm.ShowHide
@ -109,6 +111,7 @@ import com.tailscale.ipn.ui.viewModel.AppViewModel
import com.tailscale.ipn.ui.viewModel.IpnViewModel.NodeState
import com.tailscale.ipn.ui.viewModel.MainViewModel
import com.tailscale.ipn.util.FeatureFlags
import kotlinx.coroutines.flow.emptyFlow
// Navigation actions for the MainView
data class MainViewNavigation(
@ -131,11 +134,11 @@ fun MainView(
val healthIcon by viewModel.healthIcon.collectAsState()
val showDirectoryPicker = appViewModel.showDirectoryPickerInterstitial.collectAsState(null)
LaunchedEffect(showDirectoryPicker.value) {
if (showDirectoryPicker.value != null) {
appViewModel.directoryPickerLauncher?.launch(null)
}
LaunchedEffect(showDirectoryPicker.value) {
if (showDirectoryPicker.value != null) {
appViewModel.directoryPickerLauncher?.launch(null)
}
}
LoadingIndicator.Wrap {
Scaffold(contentWindowInsets = WindowInsets.Companion.statusBars) { paddingInsets ->
@ -221,14 +224,6 @@ fun MainView(
LaunchVpnPermissionIfNeeded(viewModel)
PromptForMissingPermissions(viewModel)
if (!viewModel.skipPromptsForAuthKeyLogin()) {
LaunchedEffect(state) {
if (state == Ipn.State.Running && !isAndroidTV()) {
appViewModel.checkIfTaildropDirectorySelected()
}
}
}
if (showKeyExpiry) {
ExpiryNotification(netmap = netmap, action = { viewModel.login() })
}
@ -848,11 +843,12 @@ fun Search(
}
}
}
/*
@Preview
@Composable
fun MainViewPreview() {
val appViewModel = AppViewModel(App.get())
val fakePrompt = emptyFlow<Unit>()
val appViewModel = AppViewModel(App.get(), fakePrompt)
val vm = MainViewModel(appViewModel)
MainView(
@ -866,4 +862,3 @@ fun MainViewPreview() {
vm,
appViewModel)
}
*/

@ -57,6 +57,8 @@ class AppViewModel(application: Application, private val taildropPrompt: Flow<Un
var directoryPickerLauncher: ActivityResultLauncher<Uri?>? = null
private val _showDirectoryPickerInterstitial = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val showDirectoryPickerInterstitial: SharedFlow<Unit> = _showDirectoryPickerInterstitial
private val _triggerDirectoryPicker = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val triggerDirectoryPicker: SharedFlow<Unit> = _triggerDirectoryPicker
val TAG = "AppViewModel"
init {
@ -73,6 +75,10 @@ class AppViewModel(application: Application, private val taildropPrompt: Flow<Un
}
}
fun requestDirectoryPicker() {
_triggerDirectoryPicker.tryEmit(Unit)
}
private fun prepareVpn() {
// Check if the user has granted permission yet.
if (!vpnPrepared.value) {
@ -99,7 +105,7 @@ class AppViewModel(application: Application, private val taildropPrompt: Flow<Un
TSLog.d(
"MainViewModel",
"Stored directory URI is invalid or inaccessible; launching directory picker.")
viewModelScope.launch { _showDirectoryPickerInterstitial.tryEmit(Unit) }
viewModelScope.launch { requestDirectoryPicker() }
} else {
TSLog.d("MainViewModel", "Using stored directory URI: $storedUri")
}

@ -12,7 +12,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
@ -23,7 +22,6 @@ import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.Ipn.State
import com.tailscale.ipn.ui.model.Tailcfg
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.util.AndroidTVUtil
import com.tailscale.ipn.ui.util.PeerCategorizer
import com.tailscale.ipn.ui.util.PeerSet
import com.tailscale.ipn.ui.util.TimeUtil
@ -162,6 +160,10 @@ class MainViewModel(private val appViewModel: AppViewModel) : IpnViewModel() {
}
}
viewModelScope.launch {
isVpnActive.collect { active -> TSLog.d("KARI", "🏷️ isVpnActive changed: $active") }
}
viewModelScope.launch {
_searchTerm.debounce(250L).collect { term ->
// run the search as a background task

@ -127,6 +127,8 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
@Throws(IOException::class)
override fun openFileWriter(fileName: String, offset: Long): libtailscale.OutputStream {
TSLog.d("ShareFileHelper", "openFileWriter called for $fileName\n" +
Throwable().stackTraceToString())
runBlocking { waitUntilTaildropDirReady() }
val (uri, stream) = openWriterFD(fileName, offset)
if (stream == null) {
@ -218,6 +220,7 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
@Throws(IOException::class)
override fun deleteFile(uri: String) {
runBlocking { waitUntilTaildropDirReady() }
val ctx = appContext ?: throw IOException("DeleteFile: not initialized")
val uri = Uri.parse(uri)
@ -283,6 +286,7 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
@Throws(IOException::class)
override fun openFileReader(name: String): libtailscale.InputStream {
runBlocking { waitUntilTaildropDirReady() }
val context = appContext ?: throw IOException("app context not initialized")
val rootUri = savedUri ?: throw IOException("SAF URI not initialized")
val dir =

@ -58,8 +58,6 @@ type App struct {
backend *ipnlocal.LocalBackend
ready sync.WaitGroup
backendMu sync.Mutex
backendRestartCh chan struct{}
}
func start(dataDir, directFileRoot string, appCtx AppContext) Application {

@ -32,10 +32,9 @@ const (
func newApp(dataDir, directFileRoot string, appCtx AppContext) Application {
a := &App{
directFileRoot: directFileRoot,
dataDir: dataDir,
appCtx: appCtx,
backendRestartCh: make(chan struct{}, 1),
directFileRoot: directFileRoot,
dataDir: dataDir,
appCtx: appCtx,
}
a.ready.Add(2)

Loading…
Cancel
Save