connexion status

This commit is contained in:
m
2026-09-12 10:01:54 +02:00
parent 9fed148a12
commit 7dd3bc66f5
2 changed files with 172 additions and 0 deletions
@@ -0,0 +1,98 @@
package com.vaultdrop.mobile.features.connection
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vaultdrop.mobile.auth.TokenProvider
import com.vaultdrop.mobile.domain.ServerConfigStore
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
/** Statut de connexion au serveur affiché dans le header. */
sealed interface ServerConnectionStatus {
/** Ping en cours (premier résultat pas encore reçu). */
data object Checking : ServerConnectionStatus
data object Online : ServerConnectionStatus
data object Offline : ServerConnectionStatus
/** Mode local : aucun compte, pas de synchro serveur (statut volontaire). */
data object Local : ServerConnectionStatus
}
/**
* Surveille la joignabilité du serveur configuré (ping `/health`) et expose le
* résultat pour le badge du header. Scope Activity (créé dans [VaultDropApp]),
* comme [SyncViewModel].
*
* - ne ping que lorsqu'un compte est connecté ([TokenProvider.current] non nul),
* sinon le statut reste `Local` ;
* - boucle périodique + re-test immédiat quand l'URL de base change ;
* - [checkNow] relance une vérification à la demande (appui sur le badge).
*/
@HiltViewModel
class ConnectionStatusViewModel @Inject constructor(
private val serverConfigStore: ServerConfigStore,
private val tokenProvider: TokenProvider,
) : ViewModel() {
private val _status = MutableStateFlow<ServerConnectionStatus>(ServerConnectionStatus.Checking)
val status: StateFlow<ServerConnectionStatus> = _status.asStateFlow()
private var baseUrlJob: Job? = null
private var loopJob: Job? = null
/** Démarre le moniteur une seule fois (idempotent). */
fun start() {
if (loopJob?.isActive == true) {
// Bascule sign-in/out : ré-évaluer tout de suite.
checkNow()
return
}
baseUrlJob = viewModelScope.launch {
serverConfigStore.baseUrl
.drop(1)
.distinctUntilChanged()
.collectLatest { performCheck() }
}
loopJob = viewModelScope.launch {
while (isActive) {
performCheck()
delay(CHECK_INTERVAL_MS)
}
}
}
/** Vérification immédiate (appui sur le badge). */
fun checkNow() {
viewModelScope.launch { performCheck() }
}
private suspend fun performCheck() {
if (tokenProvider.current == null) {
_status.value = ServerConnectionStatus.Local
return
}
_status.value = ServerConnectionStatus.Checking
_status.value = serverConfigStore.checkHealth(serverConfigStore.current).fold(
onSuccess = { ServerConnectionStatus.Online },
onFailure = { e ->
Timber.d(e, "connection: ping /health échoué")
ServerConnectionStatus.Offline
},
)
}
companion object {
/** Cadence du ping — même cadence que la boucle de sync. */
const val CHECK_INTERVAL_MS = 30_000L
}
}
@@ -0,0 +1,74 @@
package com.vaultdrop.mobile.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
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 com.vaultdrop.mobile.R
import com.vaultdrop.mobile.features.connection.ServerConnectionStatus
import com.vaultdrop.mobile.ui.theme.InactiveGray
private val OnlineGreen = Color(0xFF2E7D32)
private val CheckingAmber = Color(0xFFF9A825)
/**
* Pilule de statut serveur affichée dans le header : cercle coloré + libellé.
* Un appui relance une vérification immédiate du serveur.
*/
@Composable
fun ServerStatusBadge(
status: ServerConnectionStatus,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val (dotColor, labelRes) = when (status) {
ServerConnectionStatus.Checking -> CheckingAmber to R.string.status_checking
ServerConnectionStatus.Online -> OnlineGreen to R.string.status_online
ServerConnectionStatus.Offline -> MaterialTheme.colorScheme.error to R.string.status_offline
ServerConnectionStatus.Local -> InactiveGray to R.string.status_local
}
val label = stringResource(labelRes)
val tapLabel = stringResource(R.string.status_tap_to_recheck)
val shape = RoundedCornerShape(percent = 50)
Row(
modifier = modifier
.clip(shape)
.clickable(
role = Role.Button,
onClickLabel = tapLabel,
onClick = onClick,
)
.padding(horizontal = 10.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
Box(
modifier = Modifier
.size(8.dp)
.background(dotColor, CircleShape),
)
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}