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 0000000000..b673d8d28c Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/fleet_logo.png differ diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 8c53ba4661..4d97669cb2 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,6 +1,13 @@ 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" }