android: touchless auth key login (#667)

updates tailscale/corp#29482

If an authKey is detected in the mdm payload, we will now skip the
onboarding flows and several of the other non-mandatory permission
prompts.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
kari/allowlan
Jonathan Nobels 6 months ago committed by GitHub
parent b9993097fc
commit 28f1931531
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -10,6 +10,7 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.RestrictionsManager
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.net.ConnectivityManager
@ -48,7 +49,6 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import libtailscale.Libtailscale
import java.io.File
import java.io.IOException
import java.net.NetworkInterface
import java.security.GeneralSecurityException
@ -157,13 +157,16 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
if (storedUri != null && storedUri.toString().startsWith("content://")) {
startLibtailscale(storedUri.toString())
} else {
startLibtailscale(this.getFilesDir().absolutePath)
startLibtailscale(this.filesDir.absolutePath)
}
healthNotifier = HealthNotifier(Notifier.health, Notifier.state, applicationScope)
connectivityManager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
NetworkChangeCallback.monitorDnsChanges(connectivityManager, dns)
initViewModels()
applicationScope.launch {
val rm = getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager
MDMSettings.update(get(), rm)
Notifier.state.collect { _ ->
combine(Notifier.state, MDMSettings.forceEnabled.flow, Notifier.prefs, Notifier.netmap) {
state,

@ -137,6 +137,11 @@ class MainActivity : ComponentActivity() {
val rm = getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager
MDMSettings.update(App.get(), rm)
if (MDMSettings.onboardingFlow.flow.value.value == ShowHide.Hide ||
MDMSettings.authKey.flow.value.value != null) {
setIntroScreenViewed(true)
}
// (jonathan) TODO: Force the app to be portrait on small screens until we have
// proper landscape layout support
if (!isLandscapeCapable()) {
@ -367,7 +372,7 @@ class MainActivity : ComponentActivity() {
onNavigateHome = backTo("main"), backTo("userSwitcher"))
}
}
if (shouldDisplayOnboarding()) {
if (isIntroScreenViewedSet()) {
navController.navigate("intro")
setIntroScreenViewed(true)
}
@ -505,10 +510,6 @@ class MainActivity : ComponentActivity() {
lifecycleScope.launch(Dispatchers.IO) { MDMSettings.update(App.get(), restrictionsManager) }
}
override fun onStart() {
super.onStart()
}
override fun onStop() {
super.onStop()
val restrictionsManager =
@ -525,11 +526,8 @@ class MainActivity : ComponentActivity() {
startActivity(intent)
}
private fun shouldDisplayOnboarding(): Boolean {
val onboardingFlowShowHide = MDMSettings.onboardingFlow.flow.value.value
val introSeen =
getSharedPreferences("introScreen", Context.MODE_PRIVATE).getBoolean("seen", false)
return (onboardingFlowShowHide == ShowHide.Show && !introSeen)
private fun isIntroScreenViewedSet(): Boolean {
return !getSharedPreferences("introScreen", Context.MODE_PRIVATE).getBoolean("seen", false)
}
private fun setIntroScreenViewed(seen: Boolean) {

@ -215,13 +215,15 @@ fun MainView(
when (state) {
Ipn.State.Running -> {
PromptPermissionsIfNecessary()
viewModel.maybeRequestVpnPermission()
LaunchVpnPermissionIfNeeded(viewModel)
LaunchedEffect(state) {
if (state == Ipn.State.Running && !isAndroidTV()) {
viewModel.checkIfTaildropDirectorySelected()
PromptForMissingPermissions(viewModel)
if (!viewModel.skipPromptsForAuthKeyLogin()) {
LaunchedEffect(state) {
if (state == Ipn.State.Running && !isAndroidTV()) {
viewModel.checkIfTaildropDirectorySelected()
}
}
}
@ -795,7 +797,11 @@ fun ExpiryNotification(netmap: Netmap.NetworkMap?, action: () -> Unit = {}) {
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun PromptPermissionsIfNecessary() {
fun PromptForMissingPermissions(viewModel: MainViewModel) {
if (viewModel.skipPromptsForAuthKeyLogin()) {
return
}
Permissions.prompt.forEach { (permission, state) ->
ErrorDialog(
title = permission.title,

@ -126,6 +126,13 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() {
this.pingViewModel.handleDismissal()
}
// Returns true if we should skip all of the user-interactive permissions prompts
// (with the exception of the VPN permission prompt)
fun skipPromptsForAuthKeyLogin(): Boolean {
val v = MDMSettings.authKey.flow.value.value
return v != null && v != ""
}
private val peerCategorizer = PeerCategorizer()
init {
@ -219,6 +226,10 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() {
}
fun checkIfTaildropDirectorySelected() {
if (skipPromptsForAuthKeyLogin()) {
return
}
val app = App.get()
val storedUri = app.getStoredDirectoryUri()
if (storedUri == null) {

Loading…
Cancel
Save