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.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
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 org.strongswan.android.R
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import org.strongswan.android.logic.routing.RuIpListManager
|
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 problem = if (pauseActive && pauseUntil > now) null else homeProblem(state, networkAvailable)
|
||||||
val whiteListActive = whiteList.config.enabled && whiteList.status == WhiteListStatus.ACTIVE
|
val whiteListActive = whiteList.config.enabled && whiteList.status == WhiteListStatus.ACTIVE
|
||||||
val lockdown = remember { context.isVpnLockdownEnabled() }
|
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(
|
val model = HomeModel(
|
||||||
state = state,
|
state = state,
|
||||||
traffic = traffic,
|
traffic = traffic,
|
||||||
@@ -257,6 +269,7 @@ fun HomeScreen(
|
|||||||
networkKinds = networkKinds,
|
networkKinds = networkKinds,
|
||||||
unread = unreadEvents,
|
unread = unreadEvents,
|
||||||
findingFastest = findingFastest,
|
findingFastest = findingFastest,
|
||||||
|
certExpiry = certExpiry,
|
||||||
)
|
)
|
||||||
val actions = HomeActions(
|
val actions = HomeActions(
|
||||||
connect = { selected?.let { viewModel.connect(context, it) } },
|
connect = { selected?.let { viewModel.connect(context, it) } },
|
||||||
@@ -297,6 +310,11 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
details = { viewModel.openDetails(true) },
|
details = { viewModel.openDetails(true) },
|
||||||
|
certUpdate = { ShxNavigation.requestAddServer() },
|
||||||
|
certLater = {
|
||||||
|
certAlias?.let { snoozeCertExpiry(context, it) }
|
||||||
|
certSnoozes++
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
LazyColumn(
|
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 networkKinds: Pair<Boolean, Boolean>,
|
||||||
val unread: Int,
|
val unread: Int,
|
||||||
val findingFastest: Boolean,
|
val findingFastest: Boolean,
|
||||||
|
val certExpiry: CertExpiry? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
/** What Home can do. */
|
/** What Home can do. */
|
||||||
@@ -82,6 +83,8 @@ internal class HomeActions(
|
|||||||
val networkSettings: () -> Unit,
|
val networkSettings: () -> Unit,
|
||||||
val vpnSettings: () -> Unit,
|
val vpnSettings: () -> Unit,
|
||||||
val details: () -> 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. */
|
/** 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="certs_none">Здесь сертификатов нет.</string>
|
||||||
<string name="settings_certs_profiles">%1$d из профилей</string>
|
<string name="settings_certs_profiles">%1$d из профилей</string>
|
||||||
<string name="settings_certs_soon">%1$d из профилей · %2$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>
|
</resources>
|
||||||
@@ -1075,4 +1075,10 @@
|
|||||||
<string name="certs_none">No certificates here.</string>
|
<string name="certs_none">No certificates here.</string>
|
||||||
<string name="settings_certs_profiles">%1$d from profiles</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="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>
|
</resources>
|
||||||
Reference in new issue
Block a user