diff --git a/android/src/main/java/com/tailscale/ipn/MainActivity.kt b/android/src/main/java/com/tailscale/ipn/MainActivity.kt index 2e7884b..3830681 100644 --- a/android/src/main/java/com/tailscale/ipn/MainActivity.kt +++ b/android/src/main/java/com/tailscale/ipn/MainActivity.kt @@ -22,6 +22,7 @@ import com.tailscale.ipn.ui.theme.AppTheme import com.tailscale.ipn.ui.view.AboutView import com.tailscale.ipn.ui.view.BugReportView import com.tailscale.ipn.ui.view.ExitNodePicker +import com.tailscale.ipn.ui.view.MDMSettingsDebugView import com.tailscale.ipn.ui.view.MainView import com.tailscale.ipn.ui.view.MainViewNavigation import com.tailscale.ipn.ui.view.PeerDetails @@ -56,8 +57,8 @@ class MainActivity : ComponentActivity() { val settingsNav = SettingsNav( onNavigateToBugReport = { navController.navigate("bugReport") }, - onNavigateToAbout = { navController.navigate("about") } - + onNavigateToAbout = { navController.navigate("about") }, + onNavigateToMDMSettings = { navController.navigate("mdmSettings") } ) composable("main") { @@ -89,6 +90,9 @@ class MainActivity : ComponentActivity() { composable("about") { AboutView() } + composable("mdmSettings") { + MDMSettingsDebugView(manager.mdmSettings) + } } } } diff --git a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt index 517db14..e842690 100644 --- a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt +++ b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt @@ -7,31 +7,63 @@ import android.content.RestrictionsManager import com.tailscale.ipn.App class MDMSettings(private val restrictionsManager: RestrictionsManager? = null) { - // TODO(angott): implement a typed enum string array type - val hiddenNetworkDevices: List = emptyList() - fun get(setting: BooleanSetting): Boolean { - restrictionsManager?.let { restrictionsManager -> - restrictionsManager.applicationRestrictions.containsKey(setting.key) - return restrictionsManager.applicationRestrictions.getBoolean(setting.key) - } ?: run { - return App.getApplication().encryptedPrefs.getBoolean(setting.key, false) + restrictionsManager?.let { + if (it.applicationRestrictions.containsKey(setting.key)) { + return it.applicationRestrictions.getBoolean(setting.key) + } } + return App.getApplication().encryptedPrefs.getBoolean(setting.key, false) } fun get(setting: StringSetting): String? { - return App.getApplication().encryptedPrefs.getString(setting.key, null) + return restrictionsManager?.applicationRestrictions?.getString(setting.key) + ?: App.getApplication().encryptedPrefs.getString(setting.key, null) } fun get(setting: AlwaysNeverUserDecidesSetting): AlwaysNeverUserDecidesValue { - val storedString = App.getApplication().encryptedPrefs.getString(setting.key, "user-decides") - ?: "user-decides" - return AlwaysNeverUserDecidesValue.valueOf(storedString) + val storedString: String = + restrictionsManager?.applicationRestrictions?.getString(setting.key) + ?: App.getApplication().encryptedPrefs.getString(setting.key, null) + ?: "user-decides" + return when (storedString) { + "always" -> { + AlwaysNeverUserDecidesValue.Always + } + + "never" -> { + AlwaysNeverUserDecidesValue.Never + } + + else -> { + AlwaysNeverUserDecidesValue.UserDecides + } + } } fun get(setting: ShowHideSetting): ShowHideValue { - val storedString = App.getApplication().encryptedPrefs.getString(setting.key, "show") - ?: "show" - return ShowHideValue.valueOf(storedString) + val storedString: String = + restrictionsManager?.applicationRestrictions?.getString(setting.key) + ?: App.getApplication().encryptedPrefs.getString(setting.key, null) + ?: "show" + return when (storedString) { + "hide" -> { + ShowHideValue.Hide + } + + else -> { + ShowHideValue.Show + } + } + } + + fun get(setting: StringArraySetting): Array? { + restrictionsManager?.let { + if (it.applicationRestrictions.containsKey(setting.key)) { + return it.applicationRestrictions.getStringArray(setting.key) + } + } + return App.getApplication().encryptedPrefs.getStringSet(setting.key, HashSet()) + ?.toTypedArray()?.sortedArray() } } diff --git a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsDefinitions.kt b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsDefinitions.kt index 9d25940..0165382 100644 --- a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsDefinitions.kt +++ b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsDefinitions.kt @@ -17,6 +17,10 @@ enum class StringSetting(val key: String, val localizedTitle: String) { Tailnet("Tailnet", "Recommended/Required Tailnet Name"), } +enum class StringArraySetting(val key: String, val localizedTitle: String) { + HiddenNetworkDevices("HiddenNetworkDevices", "Hidden Network Device Categories") +} + // A setting representing a String value which is set to either `always`, `never` or `user-decides`. enum class AlwaysNeverUserDecidesSetting(val key: String, val localizedTitle: String) { AllowIncomingConnections("AllowIncomingConnections", "Allow Incoming Connections"), diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt new file mode 100644 index 0000000..3c2b1cf --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt @@ -0,0 +1,119 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package com.tailscale.ipn.ui.view + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import com.tailscale.ipn.R +import com.tailscale.ipn.mdm.AlwaysNeverUserDecidesSetting +import com.tailscale.ipn.mdm.BooleanSetting +import com.tailscale.ipn.mdm.MDMSettings +import com.tailscale.ipn.mdm.ShowHideSetting +import com.tailscale.ipn.mdm.StringArraySetting +import com.tailscale.ipn.mdm.StringSetting +import com.tailscale.ipn.ui.util.defaultPaddingModifier + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MDMSettingsDebugView(mdmSettings: MDMSettings) { + Scaffold( + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text(stringResource(R.string.current_mdm_settings)) + } + ) + }, + ) { innerPadding -> + LazyColumn(modifier = Modifier.padding(innerPadding)) { + items(enumValues()) { booleanSetting -> + MDMSettingView( + title = booleanSetting.localizedTitle, + caption = booleanSetting.key, + valueDescription = mdmSettings.get(booleanSetting).toString() + ) + } + + items(enumValues()) { stringSetting -> + MDMSettingView( + title = stringSetting.localizedTitle, + caption = stringSetting.key, + valueDescription = mdmSettings.get(stringSetting).toString() + ) + } + + items(enumValues()) { showHideSetting -> + MDMSettingView( + title = showHideSetting.localizedTitle, + caption = showHideSetting.key, + valueDescription = mdmSettings.get(showHideSetting).toString() + ) + } + + items(enumValues()) { anuSetting -> + MDMSettingView( + title = anuSetting.localizedTitle, + caption = anuSetting.key, + valueDescription = mdmSettings.get(anuSetting).toString() + ) + } + + items(enumValues()) { stringArraySetting -> + MDMSettingView( + title = stringArraySetting.localizedTitle, + caption = stringArraySetting.key, + valueDescription = mdmSettings.get(stringArraySetting).toString() + ) + } + } + } + +} + +@Composable +fun MDMSettingView(title: String, caption: String, valueDescription: String) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = defaultPaddingModifier() + .fillMaxWidth() + ) { + Column { + Text(title, maxLines = 3) + Text( + caption, + fontSize = MaterialTheme.typography.labelSmall.fontSize, + color = MaterialTheme.colorScheme.tertiary, + fontFamily = FontFamily.Monospace + ) + } + + Text( + valueDescription, + color = MaterialTheme.colorScheme.secondary, + fontFamily = FontFamily.Monospace, + maxLines = 1, + fontWeight = FontWeight.SemiBold + ) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt index 84db174..35ba495 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import com.tailscale.ipn.mdm.MDMSettings import com.tailscale.ipn.R import com.tailscale.ipn.ui.Links import com.tailscale.ipn.ui.model.IpnLocal @@ -44,7 +45,8 @@ import com.tailscale.ipn.ui.viewModel.SettingsViewModel data class SettingsNav( val onNavigateToBugReport: () -> Unit, - val onNavigateToAbout: () -> Unit + val onNavigateToAbout: () -> Unit, + val onNavigateToMDMSettings: () -> Unit ) @Composable diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt index 130ba9d..bb08a7a 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt @@ -74,7 +74,8 @@ class SettingsViewModel(val model: IpnModel, val ipnActions: IpnActions, val nav // General settings, always enabled SettingBundle(settings = listOf( Setting(R.string.about, SettingType.NAV, onClick = { navigation.onNavigateToAbout() }, enabled = MutableStateFlow(true)), - Setting(R.string.bug_report, SettingType.NAV, onClick = { navigation.onNavigateToBugReport() }, enabled = MutableStateFlow(true)) + Setting(R.string.bug_report, SettingType.NAV, onClick = { navigation.onNavigateToBugReport() }, enabled = MutableStateFlow(true)), + Setting(R.string.mdm_settings, SettingType.NAV, onClick = { navigation.onNavigateToMDMSettings() }, enabled = MutableStateFlow(true)) )) ) } diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 62d9807..912f26e 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -45,5 +45,9 @@ OS Key Expiry + + Current MDM Settings + MDM Settings + diff --git a/cmd/tailscale/syspolicy_handler.go b/cmd/tailscale/syspolicy_handler.go index 724bf1d..6718f37 100644 --- a/cmd/tailscale/syspolicy_handler.go +++ b/cmd/tailscale/syspolicy_handler.go @@ -1,6 +1,5 @@ -// Copyright (c) 2024 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause package main