diff --git a/android/src/main/java/com/tailscale/ipn/MainActivity.kt b/android/src/main/java/com/tailscale/ipn/MainActivity.kt index efa5ddc..31abe23 100644 --- a/android/src/main/java/com/tailscale/ipn/MainActivity.kt +++ b/android/src/main/java/com/tailscale/ipn/MainActivity.kt @@ -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 - 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)) }, diff --git a/android/src/main/java/com/tailscale/ipn/TaildropDirectoryStore.kt b/android/src/main/java/com/tailscale/ipn/TaildropDirectoryStore.kt index be02c95..c168d7d 100644 --- a/android/src/main/java/com/tailscale/ipn/TaildropDirectoryStore.kt +++ b/android/src/main/java/com/tailscale/ipn/TaildropDirectoryStore.kt @@ -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) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt index 5912b1a..abdf6ae 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt @@ -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() + val appViewModel = AppViewModel(App.get(), fakePrompt) val vm = MainViewModel(appViewModel) MainView( @@ -866,4 +862,3 @@ fun MainViewPreview() { vm, appViewModel) } -*/ diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/AppViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/AppViewModel.kt index a52bf8a..245a5c5 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/AppViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/AppViewModel.kt @@ -57,6 +57,8 @@ class AppViewModel(application: Application, private val taildropPrompt: Flow? = null private val _showDirectoryPickerInterstitial = MutableSharedFlow(extraBufferCapacity = 1) val showDirectoryPickerInterstitial: SharedFlow = _showDirectoryPickerInterstitial + private val _triggerDirectoryPicker = MutableSharedFlow(extraBufferCapacity = 1) + val triggerDirectoryPicker: SharedFlow = _triggerDirectoryPicker val TAG = "AppViewModel" init { @@ -73,6 +75,10 @@ class AppViewModel(application: Application, private val taildropPrompt: Flow TSLog.d("KARI", "🏷️ isVpnActive changed: $active") } + } + viewModelScope.launch { _searchTerm.debounce(250L).collect { term -> // run the search as a background task diff --git a/android/src/main/java/com/tailscale/ipn/util/ShareFileHelper.kt b/android/src/main/java/com/tailscale/ipn/util/ShareFileHelper.kt index 3733689..eec4958 100644 --- a/android/src/main/java/com/tailscale/ipn/util/ShareFileHelper.kt +++ b/android/src/main/java/com/tailscale/ipn/util/ShareFileHelper.kt @@ -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 = diff --git a/libtailscale/backend.go b/libtailscale/backend.go index 0b046b4..b052693 100644 --- a/libtailscale/backend.go +++ b/libtailscale/backend.go @@ -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 { diff --git a/libtailscale/tailscale.go b/libtailscale/tailscale.go index 3a785fa..ecbe0df 100644 --- a/libtailscale/tailscale.go +++ b/libtailscale/tailscale.go @@ -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)