Update UI according to feedback (#37423)
This commit is contained in:
@@ -51,7 +51,7 @@ class AgentApplication : Application() {
|
||||
ApiClient.setEnrollmentCredentials(
|
||||
enrollSecret = enrollSecret,
|
||||
hardwareUUID = hostUUID,
|
||||
baseUrl = serverURL,
|
||||
serverUrl = serverURL,
|
||||
computerName = "${Build.BRAND} ${Build.MODEL}",
|
||||
)
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ object ApiClient {
|
||||
|
||||
private lateinit var dataStore: DataStore<Preferences>
|
||||
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<String?>
|
||||
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<String?>
|
||||
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 <R, T> 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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<String>) {
|
||||
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() {
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<string name="app_name">Fleet</string>
|
||||
|
||||
<string name="app_description">The app is installed by your IT administrator to install the certificates needed to connect to corporate networks.</string>
|
||||
<string name="learn_about_fleet">About Fleet →</string>
|
||||
<string name="learn_about_fleet">About Fleet</string>
|
||||
<string name="fleet_logo">Fleet logo</string>
|
||||
<string name="certificate_list_title">Installed certificates</string>
|
||||
<string name="certificate_list_no_certificates">No certificates installed</string>
|
||||
<string name="app_version_title">App Version</string>
|
||||
|
||||
<!-- Managed Configuration -->
|
||||
|
||||
Reference in New Issue
Block a user