Fleet Android UI (#37345)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #37189 - [x] QA'd all new/changed functionality manually
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
<!-- Source: https://developers.google.com/android/management/sdk-integration#add_queries -->
|
||||
<queries>
|
||||
<package android:name="com.google.android.apps.work.clouddpc" />
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<!-- REQUIRED for downloading certificates from remote server -->
|
||||
@@ -61,4 +65,4 @@
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -56,11 +56,11 @@ object ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
val apiKeyFlow: Flow<String?>
|
||||
val apiKeyDebugFlow: Flow<String?>
|
||||
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
|
||||
|
||||
@@ -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<String>()
|
||||
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<CertificateInfo> 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<MainDestination> {
|
||||
MainScreen(
|
||||
onNavigateToDebug = { navController.navigate(DebugDestination) },
|
||||
)
|
||||
}
|
||||
|
||||
composable<DebugDestination> {
|
||||
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<String>()
|
||||
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<String>) {
|
||||
Column(modifier = modifier) {
|
||||
@@ -139,37 +239,6 @@ fun PermissionList(modifier: Modifier = Modifier, permissionsList: List<String>)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CertificateList(modifier: Modifier = Modifier, certificateList: List<CertificateInfo>) {
|
||||
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<CertificateInfo> = 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -1,6 +1,13 @@
|
||||
<resources>
|
||||
<string name="app_name">Fleet Agent</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">Learn about Fleet</string>
|
||||
<string name="fleet_logo">Fleet logo</string>
|
||||
<string name="certificate_list_title">Installed Certificates</string>
|
||||
<string name="app_version_title">App Version</string>
|
||||
|
||||
<!-- Managed Configuration -->
|
||||
<string name="enroll_secret_title">Enroll Secret</string>
|
||||
<string name="enroll_secret_description">Secret used to enroll in a fleet instance</string>
|
||||
<string name="server_url_title">Fleet Base URL</string>
|
||||
|
||||
@@ -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" }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user