@ -3,6 +3,9 @@
package com.tailscale.ipn.ui.view
import android.Manifest
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@ -36,8 +39,10 @@ import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -49,6 +54,10 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale
import com.tailscale.ipn.R
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.IpnLocal
@ -60,6 +69,7 @@ import com.tailscale.ipn.ui.util.PeerSet
import com.tailscale.ipn.ui.util.flag
import com.tailscale.ipn.ui.util.itemsWithDividers
import com.tailscale.ipn.ui.viewModel.MainViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
// Navigation actions for the MainView
@ -69,6 +79,7 @@ data class MainViewNavigation(
val onNavigateToExitNodes : ( ) -> Unit
)
@OptIn ( ExperimentalPermissionsApi :: class )
@Composable
fun MainView ( navigation : MainViewNavigation , viewModel : MainViewModel = viewModel ( ) ) {
LoadingIndicator . Wrap {
@ -108,6 +119,8 @@ fun MainView(navigation: MainViewNavigation, viewModel: MainViewModel = viewMode
when ( state . value ) {
Ipn . State . Running -> {
PromptWriteStoragePermissionsIfNecessary ( )
val selfPeerId = viewModel . selfPeerId . collectAsState ( initial = " " )
Row (
modifier =
@ -395,3 +408,34 @@ fun PeerList(
}
}
}
@OptIn ( ExperimentalPermissionsApi :: class )
@Composable
fun PromptWriteStoragePermissionsIfNecessary ( ) {
val writeStoragePermissionState =
rememberPermissionState ( Manifest . permission . WRITE _EXTERNAL _STORAGE )
val showDialog = remember { MutableStateFlow ( false ) }
val requestPermissionLauncher =
rememberLauncherForActivityResult ( ActivityResultContracts . RequestPermission ( ) ) { granted ->
if ( ! granted ) {
showDialog . value = true
}
}
LaunchedEffect ( writeStoragePermissionState ) {
if ( ! writeStoragePermissionState . status . isGranted &&
writeStoragePermissionState . status . shouldShowRationale ) {
showDialog . value = true
} else {
requestPermissionLauncher . launch ( Manifest . permission . WRITE _EXTERNAL _STORAGE )
}
}
if ( showDialog . collectAsState ( ) . value ) {
ErrorDialog ( title = R . string . permission _required , message = R . string . taildrop _requires _write ) {
showDialog . value = false
}
}
}