From cbdd4f6a7b4028a698eb3eb3cd902478cdfd7c37 Mon Sep 17 00:00:00 2001 From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:23:53 -0500 Subject: [PATCH] Fleet Android UI (#37345) **Related issue:** Resolves #37189 - [x] QA'd all new/changed functionality manually --- android/app/build.gradle.kts | 31 +- android/app/src/main/AndroidManifest.xml | 6 +- .../main/java/com/fleetdm/agent/ApiClient.kt | 4 +- .../java/com/fleetdm/agent/MainActivity.kt | 402 ++++++++++++------ .../java/com/fleetdm/agent/ui/theme/Color.kt | 3 + .../java/com/fleetdm/agent/ui/theme/Theme.kt | 33 +- .../java/com/fleetdm/agent/ui/theme/Type.kt | 1 + .../main/res/drawable-nodpi/fleet_logo.png | Bin 0 -> 4326 bytes android/app/src/main/res/values/strings.xml | 7 + android/gradle/libs.versions.toml | 25 +- 10 files changed, 341 insertions(+), 171 deletions(-) create mode 100644 android/app/src/main/res/drawable-nodpi/fleet_logo.png diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index ece8d12ab3..a21275bd52 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -7,9 +7,9 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.serialization) alias(libs.plugins.spotless) alias(libs.plugins.detekt) - id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" id("jacoco") } @@ -26,6 +26,8 @@ android { versionCode = 2 versionName = "1.0.0" + buildConfigField("String", "INFO_URL", "\"https://fleetdm.com/better\"") + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" // Pass integration test flag from project property to instrumentation runner @@ -110,6 +112,7 @@ android { } buildFeatures { compose = true + buildConfig = true } packaging { resources { @@ -143,6 +146,7 @@ kotlin { jvmToolchain(17) compilerOptions { jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11) + freeCompilerArgs.add("-opt-in=androidx.compose.material3.ExperimentalMaterial3Api") } } @@ -238,39 +242,40 @@ dependencies { implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.datastore.preferences) + implementation(libs.androidx.navigation.compose) implementation(libs.androidx.work.runtime.ktx) implementation(libs.amapi.sdk) + implementation(libs.kotlinx.serialization.json) // SCEP (Simple Certificate Enrollment Protocol) - implementation("com.google.code.jscep:jscep:3.0.1") + implementation(libs.jscep) // Bouncy Castle - Cryptography provider - implementation("org.bouncycastle:bcprov-jdk18on:1.78.1") - implementation("org.bouncycastle:bcpkix-jdk18on:1.78.1") + implementation(libs.bouncycastle.bcprov) + implementation(libs.bouncycastle.bcpkix) // Apache Commons - Utilities used by jScep - implementation("commons-codec:commons-codec:1.20.0") + implementation(libs.commons.codec) // Logging - Required by jScep - implementation("org.slf4j:slf4j-api:2.0.17") - implementation("org.slf4j:slf4j-simple:2.0.17") + implementation(libs.slf4j.api) + implementation(libs.slf4j.simple) // Testing testImplementation(libs.junit) testImplementation(libs.androidx.work.testing) testImplementation(libs.robolectric) - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") - testImplementation("org.json:json:20231013") // For JSON parsing in unit tests + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.json) // For JSON parsing in unit tests androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.compose.ui.test.junit4) - androidTestImplementation("io.mockk:mockk-android:1.13.13") + androidTestImplementation(libs.mockk.android) debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.test.manifest) - // Preferences DataStore (SharedPreferences like APIs) - implementation("androidx.datastore:datastore-preferences:1.2.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2dbd16ac2e..8dff251891 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,10 @@ + + + + @@ -61,4 +65,4 @@ - \ No newline at end of file + 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 34fe245f46..21a387ce3c 100644 --- a/android/app/src/main/java/com/fleetdm/agent/ApiClient.kt +++ b/android/app/src/main/java/com/fleetdm/agent/ApiClient.kt @@ -56,11 +56,11 @@ object ApiClient { } } - val apiKeyFlow: Flow + val apiKeyDebugFlow: Flow get() = dataStore.data.map { preferences -> preferences[API_KEY]?.let { encrypted -> try { - KeystoreManager.decrypt(encrypted) + "****" + KeystoreManager.decrypt(encrypted).takeLast(4) } catch (e: Exception) { Log.e("ApiClient", "Failed to decrypt API key", e) null 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 f3e12ae455..2d548ee4ce 100644 --- a/android/app/src/main/java/com/fleetdm/agent/MainActivity.kt +++ b/android/app/src/main/java/com/fleetdm/agent/MainActivity.kt @@ -1,131 +1,231 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + package com.fleetdm.agent import android.app.admin.DevicePolicyManager +import android.content.ClipData +import android.content.ClipboardManager import android.content.Context +import android.content.Context.DEVICE_POLICY_SERVICE +import android.content.Context.RESTRICTIONS_SERVICE +import android.content.Intent +import android.content.RestrictionsManager import android.content.pm.PackageInfo import android.content.pm.PackageManager -import android.os.Build +import android.graphics.Color import android.os.Bundle -import android.provider.Settings -import android.util.Log +import android.widget.Toast import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.core.net.toUri +import androidx.navigation.compose.NavHost +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 java.security.KeyStore -import java.security.cert.X509Certificate -import java.util.Date -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable + +const val CLICKS_TO_DEBUG = 8 + +@Serializable +object MainDestination + +@Serializable +object DebugDestination class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - enableEdgeToEdge() - - // 1. Fetch the Managed Configuration (Application Restrictions) - val restrictionsManager = getSystemService(Context.RESTRICTIONS_SERVICE) as android.content.RestrictionsManager - val appRestrictions = restrictionsManager.applicationRestrictions - val dpm = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.light( + scrim = Color.TRANSPARENT, + darkScrim = Color.TRANSPARENT, + ), + ) setContent { - val enrollSecret by remember { mutableStateOf(appRestrictions.getString("enroll_secret")) } - val delegatedScopes by remember { mutableStateOf(dpm.getDelegatedScopes(null, packageName).toList()) } - val delegatedCertScope by remember { - mutableStateOf(delegatedScopes.contains(DevicePolicyManager.DELEGATION_CERT_INSTALL)) - } - val androidID by remember { mutableStateOf(Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)) } - val enrollmentSpecificID by remember { mutableStateOf(appRestrictions.getString("host_uuid")) } - val certIds by remember { mutableStateOf(CertificateOrchestrator.getCertificateIDs(this)) } - val permissionsList by remember { - val grantedPermissions = mutableListOf() - val packageInfo: PackageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS) - packageInfo.requestedPermissions?.let { - for (i in it.indices) { - if (( - packageInfo.requestedPermissionsFlags?.get(i) - ?.and(PackageInfo.REQUESTED_PERMISSION_GRANTED) - ) != 0 - ) { - grantedPermissions.add(it[i]) - } - } - } - mutableStateOf(grantedPermissions.toList()) - } - val fleetBaseUrl by remember { - mutableStateOf(appRestrictions.getString("server_url")) - } - var installedCertificates: List by remember { mutableStateOf(listOf()) } - val apiKey by ApiClient.apiKeyFlow.collectAsState(initial = null) - val baseUrl by ApiClient.baseUrlFlow.collectAsState(initial = null) - val allegedInstalledCerts by CertificateOrchestrator.installedCertsFlow(this).collectAsState(initial = "") - - LaunchedEffect(Unit) { - installedCertificates = listKeystoreCertificates() - } - MyApplicationTheme { - Scaffold( - modifier = Modifier.fillMaxSize(), - content = { padding -> - Column( - modifier = Modifier.padding(padding).verticalScroll(rememberScrollState()), - ) { - StatusScreen() - KeyValue("packageName", packageName) - KeyValue("versionName", packageManager.getPackageInfo(packageName, 0).versionName) - KeyValue("longVersionCode", packageManager.getPackageInfo(packageName, 0).longVersionCode.toString()) - KeyValue("enroll_secret", enrollSecret) - KeyValue("delegatedScopes", delegatedScopes.toString()) - KeyValue("delegated cert scope", delegatedCertScope.toString()) - KeyValue("android id", androidID) - KeyValue("host_uuid (MC)", enrollmentSpecificID) - KeyValue("server_url (MC)", fleetBaseUrl) - KeyValue("orbit_node_key (datastore)", apiKey) - KeyValue("base_url (datastore)", baseUrl) - KeyValue("certificate_templates->id", certIds.toString()) - KeyValue("alleged_installed", allegedInstalledCerts.toString()) - PermissionList( - permissionsList = permissionsList, - ) - CertificateList(certificateList = installedCertificates) - } - }, - ) + AppNavigation() } } } } +@Composable +fun AppNavigation() { + val navController = rememberNavController() + + NavHost( + navController = navController, + startDestination = MainDestination, + ) { + composable { + MainScreen( + onNavigateToDebug = { navController.navigate(DebugDestination) }, + ) + } + + composable { + DebugScreen( + onNavigateBack = { navController.popBackStack() }, + ) + } + } +} + +@Composable +fun MainScreen(onNavigateToDebug: () -> Unit) { + val context = LocalContext.current + + var versionClicks by remember { mutableStateOf(0) } + val installedCerts by CertificateOrchestrator.installedCertsFlow(context).collectAsState(initial = emptyMap()) + + Scaffold( + modifier = Modifier.fillMaxSize(), + content = { paddingValues -> + Column(Modifier.padding(paddingValues = paddingValues)) { + LogoHeader() + HorizontalDivider() + AboutFleet { + val intent = Intent(Intent.ACTION_VIEW, BuildConfig.INFO_URL.toUri()) + if (intent.resolveActivity(context.packageManager) != null) { + // A browser is available, open the URL directly + context.startActivity(intent) + } else { + // A browser is not available in the work profile, display a toast with the URL + val toast = Toast.makeText(context, "Visit ${BuildConfig.INFO_URL}\nfor more information", Toast.LENGTH_LONG) + toast.show() + } + } + HorizontalDivider() + CertificateList(certificates = installedCerts) + HorizontalDivider() + AppVersion { + if (++versionClicks >= CLICKS_TO_DEBUG) { + onNavigateToDebug() + } else if (versionClicks == 1) { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip(ClipData.newPlainText("", "Fleet Android Agent: ${BuildConfig.VERSION_NAME}")) + Toast.makeText(context, "Fleet Agent version copied", Toast.LENGTH_SHORT).show() + } + } + } + }, + ) +} + +@Composable +fun DebugScreen(onNavigateBack: () -> Unit) { + val context = LocalContext.current + + val restrictionsManager = context.getSystemService(RESTRICTIONS_SERVICE) as RestrictionsManager + 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 { + val grantedPermissions = mutableListOf() + val packageInfo: PackageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS) + packageInfo.requestedPermissions?.let { + for (i in it.indices) { + if (( + packageInfo.requestedPermissionsFlags?.get(i) + ?.and(PackageInfo.REQUESTED_PERMISSION_GRANTED) + ) != 0 + ) { + grantedPermissions.add(it[i]) + } + } + } + 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()) + + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar( + title = { Text("Debug Information") }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + ) + } + }, + ) + }, + content = { paddingValues -> + Column( + Modifier + .padding(paddingValues = paddingValues) + .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("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("certificate_templates->id", certIds.toString()) + KeyValue("certs_installed", installedCerts.toString()) + PermissionList( + permissionsList = permissionsList, + ) + } + }, + ) +} + @Composable fun PermissionList(modifier: Modifier = Modifier, permissionsList: List) { Column(modifier = modifier) { @@ -139,37 +239,6 @@ fun PermissionList(modifier: Modifier = Modifier, permissionsList: List) } } -@Composable -fun CertificateList(modifier: Modifier = Modifier, certificateList: List) { - Column(modifier = modifier) { - Text("certificate list:") - HorizontalDivider() - certificateList.forEach { - Text(it.alias) - HorizontalDivider() - } - } -} - -@Composable -fun StatusScreen(modifier: Modifier = Modifier) { - val tag = "CertStatusScreen" - Log.i(tag, "this is a log!") - - Greeting( - name = "banana frog", - modifier = modifier, - ) -} - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier, - ) -} - @Composable fun KeyValue(key: String, value: String?) { Text( @@ -183,37 +252,94 @@ fun KeyValue(key: String, value: String?) { HorizontalDivider() } -suspend fun listKeystoreCertificates(): List = withContext(Dispatchers.IO) { - try { - val keyStore = KeyStore.getInstance("AndroidKeyStore") - keyStore.load(null) - - val aliases = keyStore.aliases().toList() - - aliases.mapNotNull { alias -> - val cert = keyStore.getCertificate(alias) as? X509Certificate - cert?.let { - CertificateInfo( - alias = alias, - subject = it.subjectDN.name, - issuer = it.issuerDN.name, - notBefore = it.notBefore, - notAfter = it.notAfter, - ) - } - } - } catch (e: Exception) { - Log.e("Certificate", "Error listing keystore certificates", e) - emptyList() +@Composable +fun AboutFleet(modifier: Modifier = Modifier, onLearnClick: () -> Unit = {}) { + Column(modifier = modifier.padding(20.dp)) { + Text( + text = stringResource(R.string.app_description), + ) + Text( + text = stringResource(R.string.learn_about_fleet), + fontWeight = FontWeight.Bold, + color = FleetTextDark, + modifier = Modifier + .padding(top = 10.dp) + .clickable(onClick = onLearnClick), + ) } } -data class CertificateInfo(val alias: String, val subject: String, val issuer: String, val notBefore: Date, val notAfter: Date) +@Composable +fun LogoHeader(modifier: Modifier = Modifier) { + Image( + modifier = modifier.padding(20.dp), + painter = painterResource(R.drawable.fleet_logo), + contentDescription = stringResource(R.string.fleet_logo), + ) +} + +@Composable +fun CertificateList(modifier: Modifier = Modifier, certificates: CertStatusMap) { + Column(modifier = modifier.padding(20.dp)) { + Text( + text = stringResource(R.string.certificate_list_title), + color = FleetTextDark, + fontWeight = FontWeight.Bold, + ) + certificates.forEach { (_, value) -> + if (value.status == CertificateInstallStatus.INSTALLED) { + Text(text = value.alias) + } + } + } +} + +@Composable +fun AppVersion(onClick: () -> Unit = {}) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + ) { + Column( + modifier = Modifier + .padding(20.dp), + ) { + Text( + text = stringResource(R.string.app_version_title), + color = FleetTextDark, + fontWeight = FontWeight.Bold, + ) + Text(text = BuildConfig.VERSION_NAME) + } + } +} @Preview(showBackground = true) @Composable -fun GreetingPreview() { +fun FleetScreenPreview() { MyApplicationTheme { - Greeting("Android") + Column { + LogoHeader() + HorizontalDivider() + AboutFleet() + HorizontalDivider() + CertificateList( + certificates = mapOf( + 1 to CertificateInstallInfo(alias = "WIFI-1", status = CertificateInstallStatus.INSTALLED), + 2 to CertificateInstallInfo(alias = "VPN-3", status = CertificateInstallStatus.FAILED), + ), + ) + HorizontalDivider() + AppVersion(onClick = {}) + } + } +} + +@Preview(showBackground = true) +@Composable +fun AboutFleetPreview() { + MyApplicationTheme { + AboutFleet() } } diff --git a/android/app/src/main/java/com/fleetdm/agent/ui/theme/Color.kt b/android/app/src/main/java/com/fleetdm/agent/ui/theme/Color.kt index c3193eed0b..5a0bdb1762 100644 --- a/android/app/src/main/java/com/fleetdm/agent/ui/theme/Color.kt +++ b/android/app/src/main/java/com/fleetdm/agent/ui/theme/Color.kt @@ -9,3 +9,6 @@ val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) val Pink40 = Color(0xFF7D5260) + +val FleetTextLight = Color(0xFF515774) +val FleetTextDark = Color(0xFF192147) diff --git a/android/app/src/main/java/com/fleetdm/agent/ui/theme/Theme.kt b/android/app/src/main/java/com/fleetdm/agent/ui/theme/Theme.kt index 2def5d8cac..e2e8733546 100644 --- a/android/app/src/main/java/com/fleetdm/agent/ui/theme/Theme.kt +++ b/android/app/src/main/java/com/fleetdm/agent/ui/theme/Theme.kt @@ -11,11 +11,11 @@ import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80, -) +// private val DarkColorScheme = darkColorScheme( +// primary = Purple80, +// secondary = PurpleGrey80, +// tertiary = Pink80, +// ) private val LightColorScheme = lightColorScheme( primary = Purple40, @@ -35,20 +35,21 @@ private val LightColorScheme = lightColorScheme( @Composable fun MyApplicationTheme( - darkTheme: Boolean = isSystemInDarkTheme(), +// darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, +// dynamicColor: Boolean = true, content: @Composable () -> Unit, ) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - darkTheme -> DarkColorScheme - else -> LightColorScheme - } + val colorScheme = LightColorScheme +// val colorScheme = when { +// dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { +// val context = LocalContext.current +// if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) +// } +// +// darkTheme -> DarkColorScheme +// else -> LightColorScheme +// } MaterialTheme( colorScheme = colorScheme, diff --git a/android/app/src/main/java/com/fleetdm/agent/ui/theme/Type.kt b/android/app/src/main/java/com/fleetdm/agent/ui/theme/Type.kt index 9abb71aa7e..eb743e25ff 100644 --- a/android/app/src/main/java/com/fleetdm/agent/ui/theme/Type.kt +++ b/android/app/src/main/java/com/fleetdm/agent/ui/theme/Type.kt @@ -16,6 +16,7 @@ val Typography = fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.5.sp, + color = FleetTextLight, ), /* Other default text styles to override titleLarge = TextStyle( diff --git a/android/app/src/main/res/drawable-nodpi/fleet_logo.png b/android/app/src/main/res/drawable-nodpi/fleet_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b673d8d28cfe8b2d2f19f5f311b7b89302eaf96c GIT binary patch literal 4326 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 Fleet Agent + The app is installed by your IT administrator to install the certificates needed to connect to corporate networks. + Learn about Fleet + Fleet logo + Installed Certificates + App Version + + Enroll Secret Secret used to enroll in a fleet instance Fleet Base URL diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 3ca78a91ab..da6ac18ad0 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.13.1" +agp = "8.13.2" kotlin = "2.2.21" coreKtx = "1.17.0" junit = "4.13.2" @@ -13,6 +13,15 @@ detekt = "1.23.8" workManager = "2.11.0" amapi = "1.7.0" robolectric = "4.16" +kotlinx = "1.9.0" +jscep = "3.0.1" +bouncycastle = "1.78.1" +commonsCodec = "1.20.0" +slf4j = "2.0.17" +datastorePreferences = "1.2.0" +navigationCompose = "2.9.6" +json = "20231013" +mockk = "1.13.13" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -33,11 +42,25 @@ androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx" androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "workManager" } amapi-sdk = { group = "com.google.android.libraries.enterprise.amapi", name = "amapi", version.ref = "amapi" } robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } +jscep = { group = "com.google.code.jscep", name = "jscep", version.ref = "jscep" } +bouncycastle-bcprov = { group = "org.bouncycastle", name = "bcprov-jdk18on", version.ref = "bouncycastle" } +bouncycastle-bcpkix = { group = "org.bouncycastle", name = "bcpkix-jdk18on", version.ref = "bouncycastle" } +commons-codec = { group = "commons-codec", name = "commons-codec", version.ref = "commonsCodec" } +slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } +slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } +androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastorePreferences" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } +androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } +kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx" } +json = { group = "org.json", name = "json", version.ref = "json" } +mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }