From 8ebec955e489984f2427766c8c88137d62521296 Mon Sep 17 00:00:00 2001 From: Andrea Gottardo Date: Tue, 12 Mar 2024 09:55:10 -0700 Subject: [PATCH] ui: add view to debug MDM settings Adds a view to see the currently set MDM settings, we're going to need this to debug actual MDM integrations more effectively. Signed-off-by: Andrea Gottardo --- .../java/com/tailscale/ipn/MainActivity.kt | 7 +- .../java/com/tailscale/ipn/mdm/MDMSettings.kt | 58 +++++++-- .../ipn/mdm/MDMSettingsDefinitions.kt | 4 + .../ipn/ui/view/MDMSettingsDebugView.kt | 119 ++++++++++++++++++ .../com/tailscale/ipn/ui/view/SettingsView.kt | 4 +- .../ipn/ui/viewModel/SettingsViewModel.kt | 3 +- android/src/main/res/values/strings.xml | 4 + 7 files changed, 183 insertions(+), 16 deletions(-) create mode 100644 android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt diff --git a/android/src/main/java/com/tailscale/ipn/MainActivity.kt b/android/src/main/java/com/tailscale/ipn/MainActivity.kt index 85f0192..c8c4650 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 @@ -58,7 +59,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") { @@ -80,6 +82,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 4122bb5..05d302b 100644 --- a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt +++ b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt @@ -4,31 +4,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") + val storedString: String = + restrictionsManager?.applicationRestrictions?.getString(setting.key) + ?: App.getApplication().encryptedPrefs.getString(setting.key, null) ?: "user-decides" - return AlwaysNeverUserDecidesValue.valueOf(storedString) + 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") + val storedString: String = + restrictionsManager?.applicationRestrictions?.getString(setting.key) + ?: App.getApplication().encryptedPrefs.getString(setting.key, null) ?: "show" - return ShowHideValue.valueOf(storedString) + 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() } } \ No newline at end of file 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 2493195..44723dd 100644 --- a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsDefinitions.kt +++ b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsDefinitions.kt @@ -14,6 +14,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..a671409 --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MDMSettingsDebugView.kt @@ -0,0 +1,119 @@ +package com.tailscale.ipn.ui.view + +import androidx.compose.foundation.clickable +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.layout.safeContentPadding +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 androidx.compose.ui.unit.dp +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 50090df..de11d4c 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 9840996..f41fee7 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 @@ -73,7 +73,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 +