@ -19,6 +19,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
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.viewModels
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeIn
@ -38,7 +39,6 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import androidx.navigation.navArgument
import com.tailscale.ipn.Peer.RequestCodes
import com.tailscale.ipn.mdm.MDMSettings
import com.tailscale.ipn.mdm.MDMSettings
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.notifier.Notifier
@ -68,6 +68,7 @@ import com.tailscale.ipn.ui.view.TailnetLockSetupView
import com.tailscale.ipn.ui.view.UserSwitcherNav
import com.tailscale.ipn.ui.view.UserSwitcherNav
import com.tailscale.ipn.ui.view.UserSwitcherView
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.SettingsNav
import com.tailscale.ipn.ui.viewModel.SettingsNav
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancel
@ -78,6 +79,8 @@ import kotlinx.coroutines.launch
class MainActivity : ComponentActivity ( ) {
class MainActivity : ComponentActivity ( ) {
private lateinit var requestVpnPermission : ActivityResultLauncher < Unit >
private lateinit var requestVpnPermission : ActivityResultLauncher < Unit >
private lateinit var navController : NavHostController
private lateinit var navController : NavHostController
private lateinit var vpnPermissionLauncher : ActivityResultLauncher < Intent >
private val viewModel : MainViewModel by viewModels ( )
companion object {
companion object {
private const val TAG = " Main Activity "
private const val TAG = " Main Activity "
@ -109,6 +112,18 @@ class MainActivity : ComponentActivity() {
installSplashScreen ( )
installSplashScreen ( )
vpnPermissionLauncher =
registerForActivityResult ( VpnPermissionContract ( ) ) { granted ->
if ( granted ) {
Log . d ( " VpnPermission " , " VPN permission granted " )
viewModel . setVpnPrepared ( true )
} else {
Log . d ( " VpnPermission " , " VPN permission denied " )
viewModel . setVpnPrepared ( false )
}
}
viewModel . setVpnPermissionLauncher ( vpnPermissionLauncher )
setContent {
setContent {
AppTheme {
AppTheme {
navController = rememberNavController ( )
navController = rememberNavController ( )
@ -176,7 +191,7 @@ class MainActivity : ComponentActivity() {
onNavigateToAuthKey = { navController . navigate ( " loginWithAuthKey " ) } )
onNavigateToAuthKey = { navController . navigate ( " loginWithAuthKey " ) } )
composable ( " main " , enterTransition = { fadeIn ( animationSpec = tween ( 150 ) ) } ) {
composable ( " main " , enterTransition = { fadeIn ( animationSpec = tween ( 150 ) ) } ) {
MainView ( loginAtUrl = :: login , navigation = mainViewNav )
MainView ( loginAtUrl = :: login , navigation = mainViewNav , viewModel = viewModel )
}
}
composable ( " settings " ) { SettingsView ( settingsNav ) }
composable ( " settings " ) { SettingsView ( settingsNav ) }
composable ( " exitNodes " ) { ExitNodePicker ( exitNodePickerNav ) }
composable ( " exitNodes " ) { ExitNodePicker ( exitNodePickerNav ) }
@ -231,13 +246,6 @@ class MainActivity : ComponentActivity() {
}
}
}
}
}
}
lifecycleScope . launch {
Notifier . state . collect { state ->
if ( state > Ipn . State . Stopped ) {
App . get ( ) . prepareVPN ( this @MainActivity , RequestCodes . requestPrepareVPN )
}
}
}
}
}
init {
init {
@ -322,10 +330,6 @@ class MainActivity : ComponentActivity() {
override fun onStart ( ) {
override fun onStart ( ) {
super . onStart ( )
super . onStart ( )
// (jonathan) TODO: Requesting VPN permissions onStart is a bit aggressive. This should
// be done when the user initiall starts the VPN
requestVpnPermission ( )
}
}
override fun onStop ( ) {
override fun onStop ( ) {
@ -335,18 +339,6 @@ class MainActivity : ComponentActivity() {
lifecycleScope . launch ( Dispatchers . IO ) { MDMSettings . update ( App . get ( ) , restrictionsManager ) }
lifecycleScope . launch ( Dispatchers . IO ) { MDMSettings . update ( App . get ( ) , restrictionsManager ) }
}
}
private fun requestVpnPermission ( ) {
val vpnIntent = VpnService . prepare ( this )
if ( vpnIntent != null ) {
val contract = VpnPermissionContract ( )
requestVpnPermission =
registerForActivityResult ( contract ) { granted ->
Log . i ( " VPN " , " VPN permission ${if (granted) "granted" else "denied"} " )
}
requestVpnPermission . launch ( Unit )
}
}
private fun openApplicationSettings ( ) {
private fun openApplicationSettings ( ) {
val intent =
val intent =
Intent ( Settings . ACTION _APP _NOTIFICATION _SETTINGS ) . apply {
Intent ( Settings . ACTION _APP _NOTIFICATION _SETTINGS ) . apply {
@ -368,9 +360,9 @@ class MainActivity : ComponentActivity() {
}
}
}
}
class VpnPermissionContract : ActivityResultContract < Uni t, Boolean > ( ) {
class VpnPermissionContract : ActivityResultContract < Inten t, Boolean > ( ) {
override fun createIntent ( context : Context , input : Uni t) : Intent {
override fun createIntent ( context : Context , input : Inten t) : Intent {
return VpnService . prepare ( context ) ?: Intent ( )
return input
}
}
override fun parseResult ( resultCode : Int , intent : Intent ? ) : Boolean {
override fun parseResult ( resultCode : Int , intent : Intent ? ) : Boolean {