android: certificates screen as in the Final 2026 mockup (from profiles, added, system; expiry colors)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b4a311c84f
commit
e465797b5f
4 files changed
+260
-94
No files matched your search
+217
-91
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
package org.strongswan.android.ui
|
package org.strongswan.android.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
@@ -16,24 +17,23 @@ import androidx.activity.compose.setContent
|
|||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.SecondaryTabRow
|
|
||||||
import androidx.compose.material3.Tab
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -42,22 +42,40 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.strongswan.android.R
|
import org.strongswan.android.R
|
||||||
import org.strongswan.android.data.VpnProfileDataSource
|
import org.strongswan.android.data.VpnProfileDataSource
|
||||||
|
import org.strongswan.android.data.VpnProfileSource
|
||||||
import org.strongswan.android.logic.StrongSwanApplication
|
import org.strongswan.android.logic.StrongSwanApplication
|
||||||
import org.strongswan.android.logic.TrustedCertificateManager
|
import org.strongswan.android.logic.TrustedCertificateManager
|
||||||
import org.strongswan.android.logic.TrustedCertificateManager.TrustedCertificateSource
|
import org.strongswan.android.logic.TrustedCertificateManager.TrustedCertificateSource
|
||||||
import org.strongswan.android.security.TrustedCertificateEntry
|
import org.strongswan.android.security.TrustedCertificateEntry
|
||||||
import org.strongswan.android.ui.compose.SecondaryScaffold
|
import org.strongswan.android.ui.compose.SecondaryScaffold
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxDialog
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxIcons
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxPanel
|
||||||
|
import org.strongswan.android.ui.compose.design.ShxTabs
|
||||||
|
import org.strongswan.android.ui.compose.theme.LocalShxColors
|
||||||
import org.strongswan.android.ui.compose.theme.ShxTheme
|
import org.strongswan.android.ui.compose.theme.ShxTheme
|
||||||
|
import org.strongswan.android.ui.compose.theme.ShxType
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class TrustedCertificatesActivity : AppCompatActivity()
|
class TrustedCertificatesActivity : AppCompatActivity()
|
||||||
{
|
{
|
||||||
@@ -86,7 +104,43 @@ class TrustedCertificatesActivity : AppCompatActivity()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
/** A CA certificate as a row of F_Certs: who uses it and how long it is valid. */
|
||||||
|
private data class CertRow(val entry: TrustedCertificateEntry, val usedBy: List<String>)
|
||||||
|
|
||||||
|
/* the three tabs of F_Certs */
|
||||||
|
private enum class CertTab { PROFILES, ADDED, SYSTEM }
|
||||||
|
|
||||||
|
/** Days a certificate counts as «expires soon» (amber). */
|
||||||
|
internal const val CERT_SOON_DAYS = 30L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The CA certificates the profiles point to, with the names of those
|
||||||
|
* profiles. Profiles that pick the CA automatically are not listed.
|
||||||
|
*/
|
||||||
|
internal fun profileCertificates(context: Context): Map<String, List<String>>
|
||||||
|
{
|
||||||
|
val source = VpnProfileSource(context)
|
||||||
|
source.open()
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return source.allVpnProfiles.mapNotNull { p -> p.certificateAlias?.let { it to (p.name ?: p.gateway) } }
|
||||||
|
.groupBy({ it.first }, { it.second })
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
source.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** For the Settings list: how many CAs come from profiles and how many of them expire soon. */
|
||||||
|
internal fun certificateSummary(context: Context): Pair<Int, Int>
|
||||||
|
{
|
||||||
|
val manager = TrustedCertificateManager.getInstance().load()
|
||||||
|
val certs = profileCertificates(context).keys.mapNotNull { manager.getCACertificateFromAlias(it) }
|
||||||
|
val soon = certs.count { TimeUnit.MILLISECONDS.toDays(it.notAfter.time - System.currentTimeMillis()) <= CERT_SOON_DAYS }
|
||||||
|
return certs.size to soon
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CertificatesScreen(
|
private fun CertificatesScreen(
|
||||||
selectMode: Boolean,
|
selectMode: Boolean,
|
||||||
@@ -95,28 +149,28 @@ private fun CertificatesScreen(
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sources = remember {
|
val c = LocalShxColors.current
|
||||||
listOf(
|
|
||||||
TrustedCertificateSource.SYSTEM to context.getString(R.string.system_tab),
|
|
||||||
TrustedCertificateSource.USER to context.getString(R.string.user_tab),
|
|
||||||
TrustedCertificateSource.LOCAL to context.getString(R.string.local_tab),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
var tab by remember { mutableIntStateOf(0) }
|
var tab by remember { mutableIntStateOf(0) }
|
||||||
var entries by remember { mutableStateOf(listOf<TrustedCertificateEntry>()) }
|
var rows by remember { mutableStateOf(listOf<CertRow>()) }
|
||||||
|
var profileCount by remember { mutableIntStateOf(0) }
|
||||||
var pendingDelete by remember { mutableStateOf<TrustedCertificateEntry?>(null) }
|
var pendingDelete by remember { mutableStateOf<TrustedCertificateEntry?>(null) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val allowImport = StrongSwanApplication.getInstance().managedConfigurationService.managedConfiguration.isAllowCertificateImport
|
val allowImport = StrongSwanApplication.getInstance().managedConfigurationService.managedConfiguration.isAllowCertificateImport
|
||||||
val importLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == android.app.Activity.RESULT_OK)
|
|
||||||
{
|
|
||||||
scope.launch { reload(sources[tab].first) { entries = it } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun load()
|
fun load()
|
||||||
{
|
{
|
||||||
scope.launch { reload(sources[tab].first) { entries = it } }
|
scope.launch {
|
||||||
|
val (list, count) = withContext(Dispatchers.IO) { loadRows(context, CertTab.entries[tab]) }
|
||||||
|
rows = list
|
||||||
|
profileCount = count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val importLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
if (it.resultCode == android.app.Activity.RESULT_OK)
|
||||||
|
{
|
||||||
|
load()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(tab) {
|
DisposableEffect(tab) {
|
||||||
@@ -126,83 +180,155 @@ private fun CertificatesScreen(
|
|||||||
onDispose { TrustedCertificateManager.getInstance().deleteObserver(observer) }
|
onDispose { TrustedCertificateManager.getInstance().deleteObserver(observer) }
|
||||||
}
|
}
|
||||||
|
|
||||||
SecondaryScaffold(
|
SecondaryScaffold(title = stringResource(R.string.trusted_certs_title), onBack = onBack) { padding ->
|
||||||
title = stringResource(R.string.trusted_certs_title),
|
Column(
|
||||||
onBack = onBack,
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
actions = {
|
modifier = Modifier.fillMaxSize().padding(padding).verticalScroll(rememberScrollState())
|
||||||
|
.padding(start = 16.dp, end = 16.dp, bottom = 24.dp),
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.certs_intro), style = ShxType.note.copy(fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight.Normal),
|
||||||
|
color = c.textSoft, modifier = Modifier.padding(horizontal = 4.dp),
|
||||||
|
)
|
||||||
|
ShxTabs(
|
||||||
|
listOf(
|
||||||
|
stringResource(R.string.certs_tab_profiles, profileCount),
|
||||||
|
stringResource(R.string.certs_tab_added),
|
||||||
|
stringResource(R.string.certs_tab_system),
|
||||||
|
),
|
||||||
|
tab, { tab = it },
|
||||||
|
)
|
||||||
|
if (rows.isEmpty())
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
stringResource(if (tab == 0) R.string.certs_none_profiles else R.string.certs_none),
|
||||||
|
style = ShxType.sub, color = c.textSub, modifier = Modifier.padding(horizontal = 6.dp, vertical = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ShxPanel()
|
||||||
|
{
|
||||||
|
rows.forEachIndexed { i, row ->
|
||||||
|
CertItem(row, onClick = {
|
||||||
|
when
|
||||||
|
{
|
||||||
|
selectMode -> onPicked(row.entry.alias)
|
||||||
|
CertTab.entries[tab] == CertTab.ADDED -> pendingDelete = row.entry
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (i < rows.lastIndex)
|
||||||
|
{
|
||||||
|
Box(Modifier.fillMaxWidth().height(1.dp).background(c.tile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (allowImport)
|
if (allowImport)
|
||||||
{
|
{
|
||||||
IconButton(onClick = {
|
Row(
|
||||||
importLauncher.launch(Intent(context, TrustedCertificateImportActivity::class.java))
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
}) { Icon(Icons.Default.Add, contentDescription = stringResource(R.string.import_certificate)) }
|
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
|
||||||
}
|
modifier = Modifier.fillMaxWidth().height(52.dp).clip(RoundedCornerShape(18.dp))
|
||||||
IconButton(onClick = {
|
.border(1.dp, c.outline, RoundedCornerShape(18.dp))
|
||||||
scope.launch {
|
.clickable(role = Role.Button) { importLauncher.launch(Intent(context, TrustedCertificateImportActivity::class.java)) },
|
||||||
withContext(Dispatchers.IO) {
|
)
|
||||||
TrustedCertificateManager.getInstance().reset().load()
|
{
|
||||||
}
|
Icon(ShxIcons.Plus, contentDescription = null, tint = c.text, modifier = Modifier.size(18.dp))
|
||||||
reload(sources[tab].first) { entries = it }
|
Text(stringResource(R.string.certs_add), style = ShxType.body.copy(fontWeight = FontWeight.Bold), color = c.text)
|
||||||
}
|
|
||||||
}) { Icon(Icons.Default.Refresh, contentDescription = stringResource(R.string.reload_trusted_certs)) }
|
|
||||||
},
|
|
||||||
) { padding ->
|
|
||||||
Column(Modifier.fillMaxSize().padding(padding)) {
|
|
||||||
SecondaryTabRow(selectedTabIndex = tab) {
|
|
||||||
sources.forEachIndexed { index, pair ->
|
|
||||||
Tab(selected = tab == index, onClick = { tab = index }, text = { Text(pair.second) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LazyColumn(Modifier.fillMaxSize()) {
|
|
||||||
items(entries, key = { it.alias }) { entry ->
|
|
||||||
ListItem(
|
|
||||||
headlineContent = { Text(entry.subjectPrimary) },
|
|
||||||
supportingContent = { Text(entry.subjectSecondary.ifEmpty { entry.alias }) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
if (selectMode) onPicked(entry.alias)
|
|
||||||
else if (sources[tab].first == TrustedCertificateSource.LOCAL) pendingDelete = entry
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.certs_system_note), style = ShxType.meta.copy(fontWeight = FontWeight.Normal, lineHeight = 17.sp),
|
||||||
|
color = c.textSub, modifier = Modifier.padding(horizontal = 6.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingDelete?.let { entry ->
|
pendingDelete?.let { entry ->
|
||||||
AlertDialog(
|
ShxDialog(
|
||||||
onDismissRequest = { pendingDelete = null },
|
title = stringResource(R.string.delete_certificate_question),
|
||||||
title = { Text(stringResource(R.string.delete_certificate_question)) },
|
paragraphs = listOf(entry.subjectPrimary),
|
||||||
text = { Text(entry.subjectPrimary) },
|
confirm = stringResource(R.string.server_delete_confirm),
|
||||||
confirmButton = {
|
dismiss = stringResource(android.R.string.cancel),
|
||||||
TextButton(onClick = {
|
onConfirm = {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
val store = KeyStore.getInstance("LocalCertificateStore")
|
val store = KeyStore.getInstance("LocalCertificateStore")
|
||||||
store.load(null, null)
|
store.load(null, null)
|
||||||
store.deleteEntry(entry.alias)
|
store.deleteEntry(entry.alias)
|
||||||
TrustedCertificateManager.getInstance().reset()
|
TrustedCertificateManager.getInstance().reset()
|
||||||
}
|
|
||||||
catch (_: Exception)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
pendingDelete = null
|
|
||||||
load()
|
|
||||||
}) { Text(stringResource(R.string.delete_profile)) }
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { pendingDelete = null }) {
|
|
||||||
Text(stringResource(android.R.string.cancel))
|
|
||||||
}
|
}
|
||||||
|
catch (_: Exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
pendingDelete = null
|
||||||
|
load()
|
||||||
},
|
},
|
||||||
|
onDismiss = { pendingDelete = null },
|
||||||
|
icon = ShxIcons.Trash,
|
||||||
|
danger = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun reload(source: TrustedCertificateSource, onResult: (List<TrustedCertificateEntry>) -> Unit)
|
@Composable
|
||||||
|
private fun CertItem(row: CertRow, onClick: () -> Unit)
|
||||||
{
|
{
|
||||||
val list = withContext(Dispatchers.IO) {
|
val c = LocalShxColors.current
|
||||||
val manager = TrustedCertificateManager.getInstance().load()
|
val notAfter = row.entry.certificate.notAfter
|
||||||
manager.getCACertificates(source).map { (alias, cert) -> TrustedCertificateEntry(alias, cert) }
|
val days = TimeUnit.MILLISECONDS.toDays(notAfter.time - System.currentTimeMillis())
|
||||||
.sorted()
|
val date = SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()).format(notAfter)
|
||||||
|
/* green while valid, amber when it expires soon, red when expired */
|
||||||
|
val (bg, fg, expColor, expText) = when
|
||||||
|
{
|
||||||
|
days < 0 -> Look(c.errorContainer, c.error, c.error, stringResource(R.string.certs_expired, date))
|
||||||
|
days <= CERT_SOON_DAYS -> Look(c.warningStrong, c.onWarning, c.warning, stringResource(R.string.certs_expires_in, days.toInt(), date))
|
||||||
|
else -> Look(c.successContainer, c.onSuccess, c.onSuccess, stringResource(R.string.certs_valid_until, SimpleDateFormat("MM.yyyy", Locale.getDefault()).format(notAfter)))
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth().clickable(onClick = onClick).padding(start = 16.dp, end = 14.dp, top = 12.dp, bottom = 12.dp),
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(40.dp).clip(RoundedCornerShape(14.dp)).background(bg))
|
||||||
|
{
|
||||||
|
Icon(ShxIcons.Cert, contentDescription = null, tint = fg, modifier = Modifier.size(20.dp))
|
||||||
|
}
|
||||||
|
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp))
|
||||||
|
{
|
||||||
|
Text(row.entry.subjectPrimary, style = ShxType.rowTitle.copy(fontSize = 15.sp), color = c.text)
|
||||||
|
val used = if (row.usedBy.isNotEmpty()) stringResource(R.string.certs_used_by, row.usedBy.joinToString(", "))
|
||||||
|
else row.entry.subjectSecondary
|
||||||
|
if (used.isNotEmpty())
|
||||||
|
{
|
||||||
|
Text(used, style = ShxType.meta.copy(fontWeight = FontWeight.Normal), color = c.textSub)
|
||||||
|
}
|
||||||
|
Text(expText, style = ShxType.meta.copy(fontWeight = FontWeight.Bold), color = expColor)
|
||||||
|
}
|
||||||
|
Icon(ShxIcons.Chevron, contentDescription = null, tint = c.textSub, modifier = Modifier.size(18.dp))
|
||||||
}
|
}
|
||||||
onResult(list)
|
}
|
||||||
|
|
||||||
|
private data class Look(val bg: Color, val fg: Color, val exp: Color, val text: String)
|
||||||
|
|
||||||
|
/** Rows of [tab] and the number of CAs used by profiles (shown on the first tab). */
|
||||||
|
private fun loadRows(context: Context, tab: CertTab): Pair<List<CertRow>, Int>
|
||||||
|
{
|
||||||
|
val manager = TrustedCertificateManager.getInstance().load()
|
||||||
|
val usage = profileCertificates(context)
|
||||||
|
val fromProfiles = usage.mapNotNull { (alias, names) ->
|
||||||
|
manager.getCACertificateFromAlias(alias)?.let { CertRow(TrustedCertificateEntry(alias, it), names) }
|
||||||
|
}.sortedBy { it.entry }
|
||||||
|
val rows = when (tab)
|
||||||
|
{
|
||||||
|
CertTab.PROFILES -> fromProfiles
|
||||||
|
CertTab.ADDED -> manager.getCACertificates(TrustedCertificateSource.LOCAL)
|
||||||
|
.map { (alias, cert) -> CertRow(TrustedCertificateEntry(alias, cert), usage[alias].orEmpty()) }.sortedBy { it.entry }
|
||||||
|
CertTab.SYSTEM -> (manager.getCACertificates(TrustedCertificateSource.SYSTEM) + manager.getCACertificates(TrustedCertificateSource.USER))
|
||||||
|
.map { (alias, cert) -> CertRow(TrustedCertificateEntry(alias, cert), usage[alias].orEmpty()) }.sortedBy { it.entry }
|
||||||
|
}
|
||||||
|
return rows to fromProfiles.size
|
||||||
}
|
}
|
||||||
+13
-1
@@ -97,6 +97,9 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import org.strongswan.android.ui.compose.components.PredictiveBackScale
|
import org.strongswan.android.ui.compose.components.PredictiveBackScale
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.strongswan.android.R
|
import org.strongswan.android.R
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -760,6 +763,10 @@ private fun SettingsRoot(onOpen: (SettingsPage) -> Unit, updateState: UpdateStat
|
|||||||
val themeMode by ShxThemeSettings.themeMode.collectAsStateWithLifecycle()
|
val themeMode by ShxThemeSettings.themeMode.collectAsStateWithLifecycle()
|
||||||
var showKillSwitchDialog by remember { mutableStateOf(false) }
|
var showKillSwitchDialog by remember { mutableStateOf(false) }
|
||||||
val certs = remember { runCatching { TrustedCertificateManager.getInstance().load().getAllCACertificates().size }.getOrDefault(0) }
|
val certs = remember { runCatching { TrustedCertificateManager.getInstance().load().getAllCACertificates().size }.getOrDefault(0) }
|
||||||
|
/* «2 from profiles · 1 expires soon» */
|
||||||
|
val certSummary by produceState(0 to 0) {
|
||||||
|
value = withContext(Dispatchers.IO) { runCatching { org.strongswan.android.ui.certificateSummary(context) }.getOrDefault(0 to 0) }
|
||||||
|
}
|
||||||
|
|
||||||
val groups = listOf(
|
val groups = listOf(
|
||||||
stringResource(R.string.settings_section_automation) to listOf(
|
stringResource(R.string.settings_section_automation) to listOf(
|
||||||
@@ -817,7 +824,12 @@ private fun SettingsRoot(onOpen: (SettingsPage) -> Unit, updateState: UpdateStat
|
|||||||
},
|
},
|
||||||
SettingsEntry(
|
SettingsEntry(
|
||||||
stringResource(R.string.trusted_certs_title),
|
stringResource(R.string.trusted_certs_title),
|
||||||
pluralStringResource(R.plurals.settings_certs_n, certs, certs),
|
when
|
||||||
|
{
|
||||||
|
certSummary.second > 0 -> stringResource(R.string.settings_certs_soon, certSummary.first, certSummary.second)
|
||||||
|
certSummary.first > 0 -> stringResource(R.string.settings_certs_profiles, certSummary.first)
|
||||||
|
else -> pluralStringResource(R.plurals.settings_certs_n, certs, certs)
|
||||||
|
},
|
||||||
ShxIcons.Cert,
|
ShxIcons.Cert,
|
||||||
) { context.startActivity(Intent(context, TrustedCertificatesActivity::class.java)) },
|
) { context.startActivity(Intent(context, TrustedCertificatesActivity::class.java)) },
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -330,7 +330,7 @@
|
|||||||
<string name="tnc_notice_details"><![CDATA[<p>Trusted Network Connect (TNC) allows server operators to assess the health of a client device.</p><p>For that purpose the server operator may request data such as a unique identifier, a list of installed packages, system settings, or cryptographic checksums of files.</p><b>Any data will be sent only after verifying the server\'s identity.</b>]]></string>
|
<string name="tnc_notice_details"><![CDATA[<p>Trusted Network Connect (TNC) allows server operators to assess the health of a client device.</p><p>For that purpose the server operator may request data such as a unique identifier, a list of installed packages, system settings, or cryptographic checksums of files.</p><b>Any data will be sent only after verifying the server\'s identity.</b>]]></string>
|
||||||
|
|
||||||
<!-- Trusted certificate selection -->
|
<!-- Trusted certificate selection -->
|
||||||
<string name="trusted_certs_title">Сертификаты CA</string>
|
<string name="trusted_certs_title">Сертификаты</string>
|
||||||
<string name="no_certificates">Нет доступных сертификатов</string>
|
<string name="no_certificates">Нет доступных сертификатов</string>
|
||||||
<string name="reload_trusted_certs">Обновить сертификат CA</string>
|
<string name="reload_trusted_certs">Обновить сертификат CA</string>
|
||||||
<string name="system_tab">Система</string>
|
<string name="system_tab">Система</string>
|
||||||
@@ -1075,4 +1075,18 @@
|
|||||||
<string name="notif_android">Звук и вид — в настройках Android</string>
|
<string name="notif_android">Звук и вид — в настройках Android</string>
|
||||||
<string name="events_channel_hints">Подсказки</string>
|
<string name="events_channel_hints">Подсказки</string>
|
||||||
<string name="events_channel_hints_desc">Предложения, например пустить новое приложение напрямую</string>
|
<string name="events_channel_hints_desc">Предложения, например пустить новое приложение напрямую</string>
|
||||||
|
<string name="certs_intro">Нужны, чтобы убедиться, что вы подключаетесь к настоящему серверу. Обычно приходят вместе с профилем — трогать ничего не нужно.</string>
|
||||||
|
<string name="certs_tab_profiles">Из профилей · %1$d</string>
|
||||||
|
<string name="certs_tab_added">Добавленные</string>
|
||||||
|
<string name="certs_tab_system">Системные</string>
|
||||||
|
<string name="certs_used_by">используют: %1$s</string>
|
||||||
|
<string name="certs_valid_until">действует до %1$s</string>
|
||||||
|
<string name="certs_expires_in">истекает через %1$d дн. — %2$s</string>
|
||||||
|
<string name="certs_expired">истёк %1$s</string>
|
||||||
|
<string name="certs_add">Добавить сертификат (.crt, .pem)</string>
|
||||||
|
<string name="certs_system_note">Системные — те, которым доверяет Android.</string>
|
||||||
|
<string name="certs_none_profiles">Профили выбирают сертификат автоматически.</string>
|
||||||
|
<string name="certs_none">Здесь сертификатов нет.</string>
|
||||||
|
<string name="settings_certs_profiles">%1$d из профилей</string>
|
||||||
|
<string name="settings_certs_soon">%1$d из профилей · %2$d скоро истекает</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -330,7 +330,7 @@
|
|||||||
<string name="tnc_notice_details"><![CDATA[<p>Trusted Network Connect (TNC) allows server operators to assess the health of a client device.</p><p>For that purpose the server operator may request data such as a unique identifier, a list of installed packages, system settings, or cryptographic checksums of files.</p><b>Any data will be sent only after verifying the server\'s identity.</b>]]></string>
|
<string name="tnc_notice_details"><![CDATA[<p>Trusted Network Connect (TNC) allows server operators to assess the health of a client device.</p><p>For that purpose the server operator may request data such as a unique identifier, a list of installed packages, system settings, or cryptographic checksums of files.</p><b>Any data will be sent only after verifying the server\'s identity.</b>]]></string>
|
||||||
|
|
||||||
<!-- Trusted certificate selection -->
|
<!-- Trusted certificate selection -->
|
||||||
<string name="trusted_certs_title">CA certificates</string>
|
<string name="trusted_certs_title">Certificates</string>
|
||||||
<string name="no_certificates">No certificates</string>
|
<string name="no_certificates">No certificates</string>
|
||||||
<string name="reload_trusted_certs">Reload CA certificates</string>
|
<string name="reload_trusted_certs">Reload CA certificates</string>
|
||||||
<string name="system_tab">System</string>
|
<string name="system_tab">System</string>
|
||||||
@@ -1061,4 +1061,18 @@
|
|||||||
<string name="notif_android">Sound and look — in Android settings</string>
|
<string name="notif_android">Sound and look — in Android settings</string>
|
||||||
<string name="events_channel_hints">Hints</string>
|
<string name="events_channel_hints">Hints</string>
|
||||||
<string name="events_channel_hints_desc">Suggestions such as letting a new app go direct</string>
|
<string name="events_channel_hints_desc">Suggestions such as letting a new app go direct</string>
|
||||||
|
<string name="certs_intro">They prove you connect to the real server. They usually come with the profile — nothing to do here.</string>
|
||||||
|
<string name="certs_tab_profiles">From profiles · %1$d</string>
|
||||||
|
<string name="certs_tab_added">Added</string>
|
||||||
|
<string name="certs_tab_system">System</string>
|
||||||
|
<string name="certs_used_by">used by: %1$s</string>
|
||||||
|
<string name="certs_valid_until">valid until %1$s</string>
|
||||||
|
<string name="certs_expires_in">expires in %1$d days — %2$s</string>
|
||||||
|
<string name="certs_expired">expired %1$s</string>
|
||||||
|
<string name="certs_add">Add certificate (.crt, .pem)</string>
|
||||||
|
<string name="certs_system_note">System ones are those Android trusts.</string>
|
||||||
|
<string name="certs_none_profiles">The profiles pick the certificate automatically.</string>
|
||||||
|
<string name="certs_none">No certificates here.</string>
|
||||||
|
<string name="settings_certs_profiles">%1$d from profiles</string>
|
||||||
|
<string name="settings_certs_soon">%1$d from profiles · %2$d expires soon</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in new issue
Block a user