android: certificate expiry card on Home as in the Final 2026 mockup
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e465797b5f
commit
ffa38d5f28
5 files changed
+149
No files matched your search
+18
@@ -118,6 +118,12 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.runtime.produceState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.strongswan.android.ui.compose.home.CertExpiry
|
||||
import org.strongswan.android.ui.compose.home.certExpiry
|
||||
import org.strongswan.android.ui.compose.home.snoozeCertExpiry
|
||||
import org.strongswan.android.R
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import org.strongswan.android.logic.routing.RuIpListManager
|
||||
@@ -243,6 +249,12 @@ fun HomeScreen(
|
||||
val problem = if (pauseActive && pauseUntil > now) null else homeProblem(state, networkAvailable)
|
||||
val whiteListActive = whiteList.config.enabled && whiteList.status == WhiteListStatus.ACTIVE
|
||||
val lockdown = remember { context.isVpnLockdownEnabled() }
|
||||
/* F_CertExpiry: the CA of the shown profile runs out soon */
|
||||
val certAlias = (state.activeProfile ?: selected)?.certificateAlias
|
||||
var certSnoozes by remember { mutableStateOf(0) }
|
||||
val certExpiry by produceState<CertExpiry?>(null, certAlias, certSnoozes) {
|
||||
value = withContext(Dispatchers.IO) { certExpiry(context, certAlias) }
|
||||
}
|
||||
val model = HomeModel(
|
||||
state = state,
|
||||
traffic = traffic,
|
||||
@@ -257,6 +269,7 @@ fun HomeScreen(
|
||||
networkKinds = networkKinds,
|
||||
unread = unreadEvents,
|
||||
findingFastest = findingFastest,
|
||||
certExpiry = certExpiry,
|
||||
)
|
||||
val actions = HomeActions(
|
||||
connect = { selected?.let { viewModel.connect(context, it) } },
|
||||
@@ -297,6 +310,11 @@ fun HomeScreen(
|
||||
}
|
||||
},
|
||||
details = { viewModel.openDetails(true) },
|
||||
certUpdate = { ShxNavigation.requestAddServer() },
|
||||
certLater = {
|
||||
certAlias?.let { snoozeCertExpiry(context, it) }
|
||||
certSnoozes++
|
||||
},
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (C) 2026 SHX VPN
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*/
|
||||
|
||||
package org.strongswan.android.ui.compose.home
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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 androidx.preference.PreferenceManager
|
||||
import org.strongswan.android.R
|
||||
import org.strongswan.android.logic.TrustedCertificateManager
|
||||
import org.strongswan.android.ui.CERT_SOON_DAYS
|
||||
import org.strongswan.android.ui.compose.theme.LocalShxColors
|
||||
import org.strongswan.android.ui.compose.theme.ShxType
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/** The CA certificate of the current profile runs out soon (F_CertExpiry). */
|
||||
internal data class CertExpiry(val alias: String, val days: Long, val notAfter: Long)
|
||||
|
||||
private const val SNOOZE_PREFIX = "pref_cert_snooze_"
|
||||
private const val SNOOZE_MS = 24L * 60 * 60 * 1000
|
||||
|
||||
/** The expiry of [alias] when it is within [CERT_SOON_DAYS] and not snoozed; blocking. */
|
||||
internal fun certExpiry(context: Context, alias: String?): CertExpiry?
|
||||
{
|
||||
alias ?: return null
|
||||
val now = System.currentTimeMillis()
|
||||
if (PreferenceManager.getDefaultSharedPreferences(context).getLong(SNOOZE_PREFIX + alias, 0L) > now)
|
||||
{
|
||||
return null
|
||||
}
|
||||
val cert = runCatching { TrustedCertificateManager.getInstance().load().getCACertificateFromAlias(alias) }.getOrNull() ?: return null
|
||||
val days = TimeUnit.MILLISECONDS.toDays(cert.notAfter.time - now)
|
||||
return if (days <= CERT_SOON_DAYS) CertExpiry(alias, days, cert.notAfter.time) else null
|
||||
}
|
||||
|
||||
/** «Later»: the card comes back tomorrow. */
|
||||
internal fun snoozeCertExpiry(context: Context, alias: String)
|
||||
{
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit()
|
||||
.putLong(SNOOZE_PREFIX + alias, System.currentTimeMillis() + SNOOZE_MS).apply()
|
||||
}
|
||||
|
||||
/** Amber card of F_CertExpiry: title, what happens when, «Update now» / «Later». */
|
||||
@Composable
|
||||
internal fun CertExpiryCard(expiry: CertExpiry, server: String, onUpdate: () -> Unit, onLater: () -> Unit)
|
||||
{
|
||||
val c = LocalShxColors.current
|
||||
val shape = RoundedCornerShape(24.dp)
|
||||
val date = SimpleDateFormat("d MMMM", Locale.getDefault()).format(Date(expiry.notAfter))
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.fillMaxWidth().clip(shape).background(c.warningContainer).border(1.dp, c.warningBorder, shape).padding(16.dp),
|
||||
)
|
||||
{
|
||||
Text(
|
||||
if (expiry.days < 0) stringResource(R.string.cert_expired_title) else stringResource(R.string.cert_expiry_title, expiry.days.toInt()),
|
||||
style = ShxType.cardTitle, color = c.onWarningStrong,
|
||||
)
|
||||
Text(
|
||||
stringResource(if (expiry.days < 0) R.string.cert_expired_text else R.string.cert_expiry_text, server, date),
|
||||
style = ShxType.note.copy(fontSize = 13.sp, lineHeight = 19.sp, fontWeight = FontWeight.Normal), color = c.onWarningContainer,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp))
|
||||
{
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.weight(2f).height(44.dp).clip(RoundedCornerShape(14.dp)).background(Color(0xFFB45309))
|
||||
.clickable(role = Role.Button, onClick = onUpdate).padding(horizontal = 14.dp),
|
||||
)
|
||||
{
|
||||
Text(stringResource(R.string.cert_update_now), style = ShxType.body.copy(fontWeight = FontWeight.ExtraBold), color = Color.White, maxLines = 1)
|
||||
}
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.weight(1f).height(44.dp).clip(RoundedCornerShape(14.dp)).border(1.dp, Color(0xFFC7A27A), RoundedCornerShape(14.dp))
|
||||
.clickable(role = Role.Button, onClick = onLater).padding(horizontal = 14.dp),
|
||||
)
|
||||
{
|
||||
Text(stringResource(R.string.cert_later), style = ShxType.body.copy(fontWeight = FontWeight.ExtraBold), color = c.onWarningStrong, maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -61,6 +61,7 @@ internal data class HomeModel(
|
||||
val networkKinds: Pair<Boolean, Boolean>,
|
||||
val unread: Int,
|
||||
val findingFastest: Boolean,
|
||||
val certExpiry: CertExpiry? = null,
|
||||
)
|
||||
|
||||
/** What Home can do. */
|
||||
@@ -82,6 +83,8 @@ internal class HomeActions(
|
||||
val networkSettings: () -> Unit,
|
||||
val vpnSettings: () -> Unit,
|
||||
val details: () -> Unit,
|
||||
val certUpdate: () -> Unit = {},
|
||||
val certLater: () -> Unit = {},
|
||||
)
|
||||
|
||||
/** Two-letter badge of a server: the exit country when known, else the profile initials. */
|
||||
@@ -335,6 +338,9 @@ internal fun HomeContent(m: HomeModel, a: HomeActions)
|
||||
}
|
||||
}
|
||||
}
|
||||
m.certExpiry?.let { expiry ->
|
||||
CertExpiryCard(expiry, profile?.name ?: profile?.gateway.orEmpty(), a.certUpdate, a.certLater)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1089,4 +1089,10 @@
|
||||
<string name="certs_none">Здесь сертификатов нет.</string>
|
||||
<string name="settings_certs_profiles">%1$d из профилей</string>
|
||||
<string name="settings_certs_soon">%1$d из профилей · %2$d скоро истекает</string>
|
||||
<string name="cert_expiry_title">Сертификат истекает через %1$d дн.</string>
|
||||
<string name="cert_expiry_text">«%1$s» перестанет подключаться %2$s. Попросите у администратора новый профиль и добавьте его.</string>
|
||||
<string name="cert_expired_title">Сертификат истёк</string>
|
||||
<string name="cert_expired_text">«%1$s» не подключается с %2$s. Попросите у администратора новый профиль и добавьте его.</string>
|
||||
<string name="cert_update_now">Обновить сейчас</string>
|
||||
<string name="cert_later">Позже</string>
|
||||
</resources>
|
||||
@@ -1075,4 +1075,10 @@
|
||||
<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>
|
||||
<string name="cert_expiry_title">Certificate expires in %1$d days</string>
|
||||
<string name="cert_expiry_text">«%1$s» stops connecting on %2$s. Ask your admin for a new profile and add it.</string>
|
||||
<string name="cert_expired_title">Certificate expired</string>
|
||||
<string name="cert_expired_text">«%1$s» no longer connects since %2$s. Ask your admin for a new profile and add it.</string>
|
||||
<string name="cert_update_now">Update now</string>
|
||||
<string name="cert_later">Later</string>
|
||||
</resources>
|
||||
Reference in new issue
Block a user