From cb0fb18fef2e0a0565c9c3652c19145e2458cd38 Mon Sep 17 00:00:00 2001 From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> Date: Thu, 18 Dec 2025 10:30:25 -0500 Subject: [PATCH] Update UI according to feedback (#37423) --- .../com/fleetdm/agent/AgentApplication.kt | 2 +- .../main/java/com/fleetdm/agent/ApiClient.kt | 30 ++----- .../fleetdm/agent/CertificateOrchestrator.kt | 9 +++ .../java/com/fleetdm/agent/MainActivity.kt | 78 ++++++++++++++----- android/app/src/main/res/values/strings.xml | 3 +- 5 files changed, 76 insertions(+), 46 deletions(-) diff --git a/android/app/src/main/java/com/fleetdm/agent/AgentApplication.kt b/android/app/src/main/java/com/fleetdm/agent/AgentApplication.kt index a85060a459..95a97610b5 100644 --- a/android/app/src/main/java/com/fleetdm/agent/AgentApplication.kt +++ b/android/app/src/main/java/com/fleetdm/agent/AgentApplication.kt @@ -51,7 +51,7 @@ class AgentApplication : Application() { ApiClient.setEnrollmentCredentials( enrollSecret = enrollSecret, hardwareUUID = hostUUID, - baseUrl = serverURL, + serverUrl = serverURL, computerName = "${Build.BRAND} ${Build.MODEL}", ) diff --git a/android/app/src/main/java/com/fleetdm/agent/ApiClient.kt b/android/app/src/main/java/com/fleetdm/agent/ApiClient.kt index 21a387ce3c..80891b0e30 100644 --- a/android/app/src/main/java/com/fleetdm/agent/ApiClient.kt +++ b/android/app/src/main/java/com/fleetdm/agent/ApiClient.kt @@ -30,7 +30,7 @@ object ApiClient { private lateinit var dataStore: DataStore private val API_KEY = stringPreferencesKey("api_key") - private val BASE_URL_KEY = stringPreferencesKey("base_url") + private val SERVER_URL_KEY = stringPreferencesKey("server_url") private val ENROLL_SECRET = stringPreferencesKey("enroll_secret") private val HARDWARE_UUID = stringPreferencesKey("hardware_uuid") private val COMPUTER_NAME = stringPreferencesKey("computer_name") @@ -50,27 +50,9 @@ object ApiClient { } } - suspend fun setBaseUrl(url: String) { - dataStore.edit { preferences -> - preferences[BASE_URL_KEY] = url - } - } - - val apiKeyDebugFlow: Flow - get() = dataStore.data.map { preferences -> - preferences[API_KEY]?.let { encrypted -> - try { - "****" + KeystoreManager.decrypt(encrypted).takeLast(4) - } catch (e: Exception) { - Log.e("ApiClient", "Failed to decrypt API key", e) - null - } - } - } - val baseUrlFlow: Flow get() = dataStore.data.map { preferences -> - preferences[BASE_URL_KEY] + preferences[SERVER_URL_KEY] } suspend fun getApiKey(): String? { @@ -83,7 +65,7 @@ object ApiClient { } } - suspend fun getBaseUrl(): String? = dataStore.data.first()[BASE_URL_KEY] + suspend fun getBaseUrl(): String? = dataStore.data.first()[SERVER_URL_KEY] private suspend fun makeRequest( endpoint: String, @@ -205,12 +187,12 @@ object ApiClient { ) } - suspend fun setEnrollmentCredentials(enrollSecret: String, hardwareUUID: String, computerName: String, baseUrl: String) { + suspend fun setEnrollmentCredentials(enrollSecret: String, hardwareUUID: String, computerName: String, serverUrl: String) { dataStore.edit { preferences -> preferences[ENROLL_SECRET] = enrollSecret preferences[HARDWARE_UUID] = hardwareUUID preferences[COMPUTER_NAME] = computerName - preferences[BASE_URL_KEY] = baseUrl + preferences[SERVER_URL_KEY] = serverUrl } } @@ -275,7 +257,7 @@ object ApiClient { val enrollSecret = prefs[ENROLL_SECRET] val hardwareUUID = prefs[HARDWARE_UUID] val computerName = prefs[COMPUTER_NAME] - val baseUrl = prefs[BASE_URL_KEY] + val baseUrl = prefs[SERVER_URL_KEY] if (enrollSecret == null || hardwareUUID == null || computerName == null || baseUrl == null) { return null diff --git a/android/app/src/main/java/com/fleetdm/agent/CertificateOrchestrator.kt b/android/app/src/main/java/com/fleetdm/agent/CertificateOrchestrator.kt index 4d90b68bba..5c3da8a2c4 100644 --- a/android/app/src/main/java/com/fleetdm/agent/CertificateOrchestrator.kt +++ b/android/app/src/main/java/com/fleetdm/agent/CertificateOrchestrator.kt @@ -60,6 +60,8 @@ object CertificateOrchestrator { private val json = Json { ignoreUnknownKeys = true encodeDefaults = true + // Treat a missing field like a null field for optional types + explicitNulls = false } // Mutex to protect concurrent access to certificate storage @@ -293,6 +295,13 @@ object CertificateOrchestrator { Log.d(TAG, "Successfully fetched certificate template: ${template.name}") + if (template.status != "delivered") { + // The certificate template hasn't failed on the device, but isn't ready to be processed yet. + // Retry next time we fetch but don't mark as failed locally + Log.i(TAG, "Certificate template ${template.name} does not have status \"delivered\": status \"${template.status}\"") + return CertificateEnrollmentHandler.EnrollmentResult.Success(template.name) + } + // Step 3: Create certificate installer (use provided or create default) val installer = certificateInstaller ?: AndroidCertificateInstaller(context) diff --git a/android/app/src/main/java/com/fleetdm/agent/MainActivity.kt b/android/app/src/main/java/com/fleetdm/agent/MainActivity.kt index 2d548ee4ce..a4b8d380d1 100644 --- a/android/app/src/main/java/com/fleetdm/agent/MainActivity.kt +++ b/android/app/src/main/java/com/fleetdm/agent/MainActivity.kt @@ -21,6 +21,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.Image 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.fillMaxSize @@ -30,6 +31,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -43,6 +45,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -59,7 +62,6 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.fleetdm.agent.ui.theme.FleetTextDark import com.fleetdm.agent.ui.theme.MyApplicationTheme -import kotlinx.coroutines.flow.map import kotlinx.serialization.Serializable const val CLICKS_TO_DEBUG = 8 @@ -136,7 +138,6 @@ fun MainScreen(onNavigateToDebug: () -> Unit) { } HorizontalDivider() CertificateList(certificates = installedCerts) - HorizontalDivider() AppVersion { if (++versionClicks >= CLICKS_TO_DEBUG) { onNavigateToDebug() @@ -159,9 +160,7 @@ fun DebugScreen(onNavigateBack: () -> Unit) { val appRestrictions = restrictionsManager.applicationRestrictions val dpm = context.getSystemService(DEVICE_POLICY_SERVICE) as DevicePolicyManager - val enrollSecret = remember { appRestrictions.getString("enroll_secret")?.let { "****" + it.takeLast(4) } } val delegatedScopes = remember { dpm.getDelegatedScopes(null, context.packageName).toList() } - val delegatedCertScope = remember { delegatedScopes.contains(DevicePolicyManager.DELEGATION_CERT_INSTALL) } val enrollmentSpecificID = remember { appRestrictions.getString("host_uuid")?.let { "****" + it.takeLast(4) } } val certIds = remember { CertificateOrchestrator.getCertificateIDs(context) } val permissionsList = remember { @@ -181,7 +180,6 @@ fun DebugScreen(onNavigateBack: () -> Unit) { grantedPermissions.toList() } val fleetBaseUrl = remember { appRestrictions.getString("server_url") } - val apiKey by ApiClient.apiKeyDebugFlow.collectAsState(initial = null) val baseUrl by ApiClient.baseUrlFlow.collectAsState(initial = null) val installedCerts by CertificateOrchestrator.installedCertsFlow(context).collectAsState(initial = emptyMap()) @@ -207,17 +205,14 @@ fun DebugScreen(onNavigateBack: () -> Unit) { .verticalScroll(rememberScrollState()), ) { KeyValue("packageName", context.packageName) - KeyValue("versionName", context.packageManager.getPackageInfo(context.packageName, 0).versionName) - KeyValue("longVersionCode", context.packageManager.getPackageInfo(context.packageName, 0).longVersionCode.toString()) - KeyValue("enroll_secret", enrollSecret) + KeyValue("versionName", BuildConfig.VERSION_NAME) + KeyValue("longVersionCode", BuildConfig.VERSION_CODE.toString()) KeyValue("delegatedScopes", delegatedScopes.toString()) - KeyValue("delegated cert scope", delegatedCertScope.toString()) KeyValue("host_uuid (MC)", enrollmentSpecificID) KeyValue("server_url (MC)", fleetBaseUrl) - KeyValue("orbit_node_key (datastore)", apiKey) - KeyValue("base_url (datastore)", baseUrl) + KeyValue("server_url (DS)", baseUrl) KeyValue("certificate_templates->id", certIds.toString()) - KeyValue("certs_installed", installedCerts.toString()) + DebugCertificateList(certificates = installedCerts) PermissionList( permissionsList = permissionsList, ) @@ -226,6 +221,28 @@ fun DebugScreen(onNavigateBack: () -> Unit) { ) } +@Composable +fun DebugCertificateList(certificates: CertStatusMap) { + Column { + Text("certificate status:", fontWeight = FontWeight.Bold) + certificates.forEach { (key, value) -> + Row(modifier = Modifier.padding(bottom = 5.dp, start = 10.dp)) { + Text( + text = key.toString(), + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(end = 5.dp), + ) + Column { + Text(text = "alias: ${value.alias}") + Text(text = "status: ${value.status}") + Text(text = "retries: ${value.retries}") + } + } + } + HorizontalDivider() + } +} + @Composable fun PermissionList(modifier: Modifier = Modifier, permissionsList: List) { Column(modifier = modifier) { @@ -258,14 +275,20 @@ fun AboutFleet(modifier: Modifier = Modifier, onLearnClick: () -> Unit = {}) { Text( text = stringResource(R.string.app_description), ) - Text( - text = stringResource(R.string.learn_about_fleet), - fontWeight = FontWeight.Bold, - color = FleetTextDark, + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier .padding(top = 10.dp) .clickable(onClick = onLearnClick), - ) + ) { + Text( + text = stringResource(R.string.learn_about_fleet), + fontWeight = FontWeight.Bold, + color = FleetTextDark, + ) + Icon(imageVector = Icons.AutoMirrored.Default.ArrowForward, contentDescription = "forward arrow") + } } } @@ -280,12 +303,15 @@ fun LogoHeader(modifier: Modifier = Modifier) { @Composable fun CertificateList(modifier: Modifier = Modifier, certificates: CertStatusMap) { - Column(modifier = modifier.padding(20.dp)) { + Column(modifier = modifier.padding(all = 20.dp)) { Text( text = stringResource(R.string.certificate_list_title), color = FleetTextDark, fontWeight = FontWeight.Bold, ) + certificates.ifEmpty { + Text(text = stringResource(R.string.certificate_list_no_certificates)) + } certificates.forEach { (_, value) -> if (value.status == CertificateInstallStatus.INSTALLED) { Text(text = value.alias) @@ -303,7 +329,7 @@ fun AppVersion(onClick: () -> Unit = {}) { ) { Column( modifier = Modifier - .padding(20.dp), + .padding(horizontal = 20.dp), ) { Text( text = stringResource(R.string.app_version_title), @@ -330,12 +356,24 @@ fun FleetScreenPreview() { 2 to CertificateInstallInfo(alias = "VPN-3", status = CertificateInstallStatus.FAILED), ), ) - HorizontalDivider() AppVersion(onClick = {}) } } } +@Preview(showBackground = true) +@Composable +fun DebugCertificateListPreview() { + MyApplicationTheme { + DebugCertificateList( + certificates = mapOf( + 1 to CertificateInstallInfo(alias = "WIFI-1", status = CertificateInstallStatus.INSTALLED), + 2 to CertificateInstallInfo(alias = "VPN-3", status = CertificateInstallStatus.FAILED), + ), + ) + } +} + @Preview(showBackground = true) @Composable fun AboutFleetPreview() { diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 724ee5addf..1e6261517b 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -2,9 +2,10 @@ Fleet The app is installed by your IT administrator to install the certificates needed to connect to corporate networks. - About Fleet → + About Fleet Fleet logo Installed certificates + No certificates installed App Version