// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package com.tailscale.ipn.ui.viewModel import androidx.annotation.StringRes import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.tailscale.ipn.BuildConfig import com.tailscale.ipn.R import com.tailscale.ipn.mdm.MDMSettings import com.tailscale.ipn.mdm.StringSetting import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.ui.util.set import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch enum class SettingType { NAV, SWITCH, NAV_WITH_TEXT, TEXT } class ComposableStringFormatter(@StringRes val stringRes: Int, vararg val params: Any) { @Composable fun getString(): String = stringResource(id = stringRes, *params) } // Represents a bundle of settings values that should be grouped together under a title data class SettingBundle(val title: String? = null, val settings: List) // Represents a UI setting. // title: The title of the setting // type: The type of setting // enabled: Whether the setting is enabled // value: The value of the setting for textual settings // isOn: The value of the setting for switch settings // onClick: The action to take when the setting is clicked (typicall for navigation) // onToggle: The action to take when the setting is toggled (typically for switches) // // Behavior is undefined if you mix the types here. Switch settings should supply an // isOn and onToggle, while navigation settings should supply an onClick and an optional // value data class Setting( val title: ComposableStringFormatter, val type: SettingType, val destructive: Boolean = false, val enabled: StateFlow = MutableStateFlow(true), val value: StateFlow? = null, val isOn: StateFlow? = null, val onClick: () -> Unit = {}, val onToggle: (Boolean) -> Unit = {} ) { constructor( titleRes: Int, type: SettingType, enabled: StateFlow = MutableStateFlow(false), value: StateFlow? = null, isOn: StateFlow? = null, onClick: () -> Unit = {}, onToggle: (Boolean) -> Unit = {} ) : this( title = ComposableStringFormatter(titleRes), type = type, enabled = enabled, value = value, isOn = isOn, onClick = onClick, onToggle = onToggle) } data class SettingsNav( val onNavigateToBugReport: () -> Unit, val onNavigateToAbout: () -> Unit, val onNavigateToMDMSettings: () -> Unit, val onNavigateToManagedBy: () -> Unit, val onNavigateToUserSwitcher: () -> Unit, val onBackPressed: () -> Unit, ) class SettingsViewModelFactory(private val navigation: SettingsNav) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return SettingsViewModel(navigation) as T } } class SettingsViewModel(val navigation: SettingsNav) : IpnViewModel() { // Display name for the logged in user var isAdmin: StateFlow = MutableStateFlow(false) val useDNSSetting = Setting( R.string.use_ts_dns, SettingType.SWITCH, isOn = MutableStateFlow(Notifier.prefs.value?.CorpDNS), onToggle = { toggleCorpDNS { // (jonathan) TODO: Error handling } }) val settings: StateFlow> = MutableStateFlow(emptyList()) init { viewModelScope.launch { // Monitor our prefs for changes and update the displayed values accordingly Notifier.prefs.collect { prefs -> useDNSSetting.isOn?.set(prefs?.CorpDNS) useDNSSetting.enabled.set(prefs != null) } } viewModelScope.launch { mdmSettings.collect { mdmSettings -> settings.set( listOf( SettingBundle( settings = listOf( useDNSSetting, )), // General settings, always enabled SettingBundle(settings = footerSettings(mdmSettings)))) } } viewModelScope.launch { Notifier.netmap.collect { netmap -> isAdmin.set(netmap?.SelfNode?.isAdmin ?: false) } } } private fun footerSettings(mdmSettings: MDMSettings): List = listOfNotNull( Setting( titleRes = R.string.about, SettingType.NAV, onClick = { navigation.onNavigateToAbout() }, enabled = MutableStateFlow(true)), Setting( titleRes = R.string.bug_report, SettingType.NAV, onClick = { navigation.onNavigateToBugReport() }, enabled = MutableStateFlow(true)), mdmSettings.get(StringSetting.ManagedByOrganizationName)?.let { Setting( ComposableStringFormatter(R.string.managed_by_orgName, it), SettingType.NAV, onClick = { navigation.onNavigateToManagedBy() }, enabled = MutableStateFlow(true)) }, if (BuildConfig.DEBUG) { Setting( titleRes = R.string.mdm_settings, SettingType.NAV, onClick = { navigation.onNavigateToMDMSettings() }, enabled = MutableStateFlow(true)) } else { null }) }