android: make currentDir reactive (#661)

-The composables were reading the currentDir value once and not observing it. This fixes it so that we recompose when the StateFlow changes.
-Use commit() instead of apply() when writing to EncryptedSharedPreferences since we are reading from it immediately and need the writes to happen synchronously
-Remove unused function in PermissionsViewModel

Fixes tailscale/corp#29283

Signed-off-by: kari-ts <kari@tailscale.com>
pull/662/head
kari-ts 6 months ago committed by GitHub
parent 87f0e9754b
commit 1ec621c382
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -25,6 +25,7 @@ import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.LinearOutSlowInEasing
@ -85,6 +86,7 @@ import com.tailscale.ipn.ui.view.UserSwitcherView
import com.tailscale.ipn.ui.viewModel.ExitNodePickerNav import com.tailscale.ipn.ui.viewModel.ExitNodePickerNav
import com.tailscale.ipn.ui.viewModel.MainViewModel import com.tailscale.ipn.ui.viewModel.MainViewModel
import com.tailscale.ipn.ui.viewModel.MainViewModelFactory import com.tailscale.ipn.ui.viewModel.MainViewModelFactory
import com.tailscale.ipn.ui.viewModel.PermissionsViewModel
import com.tailscale.ipn.ui.viewModel.PingViewModel import com.tailscale.ipn.ui.viewModel.PingViewModel
import com.tailscale.ipn.ui.viewModel.SettingsNav import com.tailscale.ipn.ui.viewModel.SettingsNav
import com.tailscale.ipn.ui.viewModel.VpnViewModel import com.tailscale.ipn.ui.viewModel.VpnViewModel
@ -105,6 +107,7 @@ class MainActivity : ComponentActivity() {
ViewModelProvider(this, MainViewModelFactory(vpnViewModel)).get(MainViewModel::class.java) ViewModelProvider(this, MainViewModelFactory(vpnViewModel)).get(MainViewModel::class.java)
} }
private lateinit var vpnViewModel: VpnViewModel private lateinit var vpnViewModel: VpnViewModel
val permissionsViewModel: PermissionsViewModel by viewModels()
companion object { companion object {
private const val TAG = "Main Activity" private const val TAG = "Main Activity"
@ -179,6 +182,7 @@ class MainActivity : ComponentActivity() {
try { try {
Libtailscale.setDirectFileRoot(uri.toString()) Libtailscale.setDirectFileRoot(uri.toString())
TaildropDirectoryStore.saveFileDirectory(uri) TaildropDirectoryStore.saveFileDirectory(uri)
permissionsViewModel.refreshCurrentDir()
} catch (e: Exception) { } catch (e: Exception) {
TSLog.e("MainActivity", "Failed to set Taildrop root: $e") TSLog.e("MainActivity", "Failed to set Taildrop root: $e")
} }
@ -333,7 +337,8 @@ class MainActivity : ComponentActivity() {
{ navController.navigate("notifications") }) { navController.navigate("notifications") })
} }
composable("taildropDir") { composable("taildropDir") {
TaildropDirView(backTo("permissions"), directoryPickerLauncher) TaildropDirView(
backTo("permissions"), directoryPickerLauncher, permissionsViewModel)
} }
composable("notifications") { composable("notifications") {
NotificationsView(backTo("permissions"), ::openApplicationSettings) NotificationsView(backTo("permissions"), ::openApplicationSettings)

@ -15,7 +15,7 @@ object TaildropDirectoryStore {
@Throws(IOException::class, GeneralSecurityException::class) @Throws(IOException::class, GeneralSecurityException::class)
fun saveFileDirectory(directoryUri: Uri) { fun saveFileDirectory(directoryUri: Uri) {
val prefs = App.get().getEncryptedPrefs() val prefs = App.get().getEncryptedPrefs()
prefs.edit().putString(PREF_KEY_SAF_URI, directoryUri.toString()).apply() prefs.edit().putString(PREF_KEY_SAF_URI, directoryUri.toString()).commit()
try { try {
// Must restart Tailscale because a new LocalBackend with the new directory must be created. // Must restart Tailscale because a new LocalBackend with the new directory must be created.
App.get().startLibtailscale(directoryUri.toString()) App.get().startLibtailscale(directoryUri.toString())

@ -13,6 +13,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -73,7 +74,9 @@ fun PermissionsView(
}, },
supportingContent = { supportingContent = {
val displayPath = val displayPath =
permissionsViewModel.currentDir.value?.let { friendlyDirName(it) } ?: "No access" permissionsViewModel.currentDir.collectAsState().value?.let {
friendlyDirName(it)
} ?: "No access"
Text(displayPath) Text(displayPath)
}) })

@ -15,21 +15,23 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.tailscale.ipn.R import com.tailscale.ipn.R
import com.tailscale.ipn.ui.theme.exitNodeToggleButton import com.tailscale.ipn.ui.theme.exitNodeToggleButton
import com.tailscale.ipn.ui.util.Lists import com.tailscale.ipn.ui.util.Lists
import com.tailscale.ipn.ui.util.friendlyDirName import com.tailscale.ipn.ui.util.friendlyDirName
import com.tailscale.ipn.ui.viewModel.PermissionsViewModel import com.tailscale.ipn.ui.viewModel.PermissionsViewModel
import com.tailscale.ipn.util.TSLog
@Composable @Composable
fun TaildropDirView( fun TaildropDirView(
backToPermissionsView: BackNavigation, backToPermissionsView: BackNavigation,
openDirectoryLauncher: ActivityResultLauncher<Uri?>, openDirectoryLauncher: ActivityResultLauncher<Uri?>,
permissionsViewModel: PermissionsViewModel = viewModel() permissionsViewModel: PermissionsViewModel
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {
@ -53,7 +55,8 @@ fun TaildropDirView(
item("divider0") { Lists.SectionDivider() } item("divider0") { Lists.SectionDivider() }
item { item {
val currentDir = permissionsViewModel.currentDir.value val currentDir by permissionsViewModel.currentDir.collectAsState()
TSLog.d("TaildropDirView", "currentDir in UI: $currentDir")
val displayPath = currentDir?.let { friendlyDirName(it) } ?: "No access" val displayPath = currentDir?.let { friendlyDirName(it) } ?: "No access"
ListItem( ListItem(

@ -3,37 +3,20 @@
package com.tailscale.ipn.ui.viewModel package com.tailscale.ipn.ui.viewModel
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.tailscale.ipn.TaildropDirectoryStore import com.tailscale.ipn.TaildropDirectoryStore
import com.tailscale.ipn.util.TSLog
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import libtailscale.Libtailscale
class PermissionsViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
class PermissionsViewModel : ViewModel() {
private val _currentDir = private val _currentDir =
MutableStateFlow<String?>(TaildropDirectoryStore.loadSavedDir().toString()) MutableStateFlow<String?>(TaildropDirectoryStore.loadSavedDir()?.toString())
val currentDir: StateFlow<String?> = _currentDir val currentDir: StateFlow<String?> = _currentDir
fun onDirectoryPicked(uri: Uri?, context: Context) { fun refreshCurrentDir() {
if (uri == null) return val newUri = TaildropDirectoryStore.loadSavedDir()?.toString()
TSLog.d("PermissionsViewModel", "refreshCurrentDir: $newUri")
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION _currentDir.value = newUri
val cr = context.contentResolver
// Revoke previous grant so you dont leak one
_currentDir.value?.let { old ->
runCatching { cr.releasePersistableUriPermission(Uri.parse(old), flags) }
}
cr.takePersistableUriPermission(uri, flags) // may throw SecurityException
Libtailscale.setDirectFileRoot(uri.toString())
TaildropDirectoryStore.saveFileDirectory(uri)
_currentDir.value = uri.toString()
} }
} }

Loading…
Cancel
Save