Skip to content

Commit 357e3ec

Browse files
committed
feat(android): add v2 about update status
1 parent f359299 commit 357e3ec

4 files changed

Lines changed: 131 additions & 34 deletions

File tree

apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ai.openclaw.app.chat.ChatPendingToolCall
55
import ai.openclaw.app.chat.ChatSessionEntry
66
import ai.openclaw.app.chat.OutgoingAttachment
77
import ai.openclaw.app.gateway.GatewayEndpoint
8+
import ai.openclaw.app.gateway.GatewayUpdateAvailableSummary
89
import ai.openclaw.app.node.CameraCaptureManager
910
import ai.openclaw.app.node.CanvasController
1011
import ai.openclaw.app.node.SmsManager
@@ -81,6 +82,8 @@ class MainViewModel(
8182
val statusText: StateFlow<String> = runtimeState(initial = "Offline") { it.statusText }
8283
val serverName: StateFlow<String?> = runtimeState(initial = null) { it.serverName }
8384
val remoteAddress: StateFlow<String?> = runtimeState(initial = null) { it.remoteAddress }
85+
val gatewayVersion: StateFlow<String?> = runtimeState(initial = null) { it.gatewayVersion }
86+
val gatewayUpdateAvailable: StateFlow<GatewayUpdateAvailableSummary?> = runtimeState(initial = null) { it.gatewayUpdateAvailable }
8487
val modelCatalog: StateFlow<List<GatewayModelSummary>> = runtimeState(initial = emptyList()) { it.modelCatalog }
8588
val modelAuthProviders: StateFlow<List<GatewayModelProviderSummary>> = runtimeState(initial = emptyList()) { it.modelAuthProviders }
8689
val modelCatalogRefreshing: StateFlow<Boolean> = runtimeState(initial = false) { it.modelCatalogRefreshing }

apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import ai.openclaw.app.gateway.GatewayEndpoint
1212
import ai.openclaw.app.gateway.GatewaySession
1313
import ai.openclaw.app.gateway.GatewayTlsProbeFailure
1414
import ai.openclaw.app.gateway.GatewayTlsProbeResult
15+
import ai.openclaw.app.gateway.GatewayUpdateAvailableSummary
1516
import ai.openclaw.app.gateway.normalizeGatewayTlsFingerprint
1617
import ai.openclaw.app.gateway.probeGatewayTlsFingerprint
1718
import ai.openclaw.app.node.A2UIHandler
@@ -295,6 +296,12 @@ class NodeRuntime(
295296
private val _remoteAddress = MutableStateFlow<String?>(null)
296297
val remoteAddress: StateFlow<String?> = _remoteAddress.asStateFlow()
297298

299+
private val _gatewayVersion = MutableStateFlow<String?>(null)
300+
val gatewayVersion: StateFlow<String?> = _gatewayVersion.asStateFlow()
301+
302+
private val _gatewayUpdateAvailable = MutableStateFlow<GatewayUpdateAvailableSummary?>(null)
303+
val gatewayUpdateAvailable: StateFlow<GatewayUpdateAvailableSummary?> = _gatewayUpdateAvailable.asStateFlow()
304+
298305
private val _seamColorArgb = MutableStateFlow(DEFAULT_SEAM_COLOR_ARGB)
299306
val seamColorArgb: StateFlow<Long> = _seamColorArgb.asStateFlow()
300307
private val _modelCatalog = MutableStateFlow<List<GatewayModelSummary>>(emptyList())
@@ -377,13 +384,15 @@ class NodeRuntime(
377384
scope = scope,
378385
identityStore = identityStore,
379386
deviceAuthStore = deviceAuthStore,
380-
onConnected = { name, remote, mainSessionKey ->
387+
onConnected = { hello ->
381388
operatorConnected = true
382389
operatorStatusText = "Connected"
383-
_serverName.value = name
384-
_remoteAddress.value = remote
390+
_serverName.value = hello.serverName
391+
_remoteAddress.value = hello.remoteAddress
392+
_gatewayVersion.value = hello.serverVersion
393+
_gatewayUpdateAvailable.value = hello.updateAvailable
385394
_seamColorArgb.value = DEFAULT_SEAM_COLOR_ARGB
386-
syncMainSessionKey(resolveAgentIdFromMainSessionKey(mainSessionKey))
395+
syncMainSessionKey(resolveAgentIdFromMainSessionKey(hello.mainSessionKey))
387396
updateStatus()
388397
micCapture.onGatewayConnectionChanged(true)
389398
scope.launch {
@@ -398,6 +407,8 @@ class NodeRuntime(
398407
operatorStatusText = message
399408
_serverName.value = null
400409
_remoteAddress.value = null
410+
_gatewayVersion.value = null
411+
_gatewayUpdateAvailable.value = null
401412
_seamColorArgb.value = DEFAULT_SEAM_COLOR_ARGB
402413
_modelCatalog.value = emptyList()
403414
_modelAuthProviders.value = emptyList()
@@ -429,7 +440,7 @@ class NodeRuntime(
429440
scope = scope,
430441
identityStore = identityStore,
431442
deviceAuthStore = deviceAuthStore,
432-
onConnected = { _, _, _ ->
443+
onConnected = {
433444
_nodeConnected.value = true
434445
nodeStatusText = "Connected"
435446
didAutoRequestCanvasRehydrate = false
@@ -1585,11 +1596,28 @@ class NodeRuntime(
15851596
event: String,
15861597
payloadJson: String?,
15871598
) {
1599+
if (event == "update.available") {
1600+
_gatewayUpdateAvailable.value = parseGatewayUpdateAvailable(payloadJson)
1601+
}
15881602
micCapture.handleGatewayEvent(event, payloadJson)
15891603
talkMode.handleGatewayEvent(event, payloadJson)
15901604
chat.handleGatewayEvent(event, payloadJson)
15911605
}
15921606

1607+
private fun parseGatewayUpdateAvailable(payloadJson: String?): GatewayUpdateAvailableSummary? {
1608+
return try {
1609+
val root = payloadJson?.let { json.parseToJsonElement(it).asObjectOrNull() }
1610+
val update = root?.get("updateAvailable").asObjectOrNull() ?: return null
1611+
GatewayUpdateAvailableSummary(
1612+
currentVersion = update["currentVersion"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
1613+
latestVersion = update["latestVersion"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
1614+
channel = update["channel"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
1615+
)
1616+
} catch (_: Throwable) {
1617+
null
1618+
}
1619+
}
1620+
15931621
private fun parseChatSendRunId(response: String): String? {
15941622
return try {
15951623
val root = json.parseToJsonElement(response).asObjectOrNull() ?: return null

apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,20 @@ data class GatewayConnectErrorDetails(
6868
val reason: String? = null,
6969
)
7070

71+
data class GatewayHelloSummary(
72+
val serverName: String?,
73+
val remoteAddress: String?,
74+
val serverVersion: String?,
75+
val mainSessionKey: String?,
76+
val updateAvailable: GatewayUpdateAvailableSummary?,
77+
)
78+
79+
data class GatewayUpdateAvailableSummary(
80+
val currentVersion: String?,
81+
val latestVersion: String?,
82+
val channel: String?,
83+
)
84+
7185
private data class SelectedConnectAuth(
7286
val authToken: String?,
7387
val authBootstrapToken: String?,
@@ -86,7 +100,7 @@ class GatewaySession(
86100
private val scope: CoroutineScope,
87101
private val identityStore: DeviceIdentityStore,
88102
private val deviceAuthStore: DeviceAuthTokenStore,
89-
private val onConnected: (serverName: String?, remoteAddress: String?, mainSessionKey: String?) -> Unit,
103+
private val onConnected: (GatewayHelloSummary) -> Unit,
90104
private val onDisconnected: (message: String) -> Unit,
91105
private val onEvent: (event: String, payloadJson: String?) -> Unit,
92106
private val onInvoke: (suspend (InvokeRequest) -> InvokeResult)? = null,
@@ -647,7 +661,9 @@ class GatewaySession(
647661
pendingDeviceTokenRetry = false
648662
deviceTokenRetryBudgetUsed = false
649663
reconnectPausedForAuthFailure = false
650-
val serverName = obj["server"].asObjectOrNull()?.get("host").asStringOrNull()
664+
val server = obj["server"].asObjectOrNull()
665+
val serverName = server?.get("host").asStringOrNull()
666+
val serverVersion = server?.get("version").asStringOrNull()
651667
val authObj = obj["auth"].asObjectOrNull()
652668
val deviceToken = authObj?.get("deviceToken").asStringOrNull()
653669
val authRole = authObj?.get("role").asStringOrNull() ?: options.role
@@ -685,13 +701,33 @@ class GatewaySession(
685701
?.let { normalized -> surface to normalized }
686702
} ?: emptyList()
687703
pluginSurfaceUrls = normalizedPluginSurfaceUrls.toMap()
704+
val snapshot = obj["snapshot"].asObjectOrNull()
688705
val sessionDefaults =
689-
obj["snapshot"]
690-
.asObjectOrNull()
706+
snapshot
691707
?.get("sessionDefaults")
692708
.asObjectOrNull()
693709
mainSessionKey = sessionDefaults?.get("mainSessionKey").asStringOrNull()
694-
onConnected(serverName, remoteAddress, mainSessionKey)
710+
onConnected(
711+
GatewayHelloSummary(
712+
serverName = serverName,
713+
remoteAddress = remoteAddress,
714+
serverVersion = serverVersion,
715+
mainSessionKey = mainSessionKey,
716+
updateAvailable = parseUpdateAvailable(snapshot?.get("updateAvailable").asObjectOrNull()),
717+
),
718+
)
719+
}
720+
721+
private fun parseUpdateAvailable(value: JsonObject?): GatewayUpdateAvailableSummary? {
722+
if (value == null) return null
723+
val latestVersion = value["latestVersion"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
724+
val currentVersion = value["currentVersion"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
725+
val channel = value["channel"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
726+
return GatewayUpdateAvailableSummary(
727+
currentVersion = currentVersion,
728+
latestVersion = latestVersion,
729+
channel = channel,
730+
)
695731
}
696732

697733
private fun buildConnectParams(

apps/android/app/src/main/java/ai/openclaw/app/ui/V2SettingsScreens.kt

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import ai.openclaw.app.ui.design.ClawStatusPill
1818
import ai.openclaw.app.ui.design.ClawTextField
1919
import ai.openclaw.app.ui.design.ClawTheme
2020
import androidx.compose.foundation.BorderStroke
21-
import androidx.compose.foundation.background
2221
import androidx.compose.foundation.layout.Arrangement
2322
import androidx.compose.foundation.layout.Box
2423
import androidx.compose.foundation.layout.Column
@@ -60,7 +59,6 @@ import androidx.compose.runtime.remember
6059
import androidx.compose.runtime.setValue
6160
import androidx.compose.ui.Alignment
6261
import androidx.compose.ui.Modifier
63-
import androidx.compose.ui.draw.clip
6462
import androidx.compose.ui.graphics.Color
6563
import androidx.compose.ui.graphics.vector.ImageVector
6664
import androidx.compose.ui.text.style.TextOverflow
@@ -111,7 +109,7 @@ internal fun V2SettingsDetailScreen(
111109
V2SettingsRoute.Gateway -> V2GatewaySettingsScreen(viewModel = viewModel, onBack = onBack)
112110
V2SettingsRoute.Appearance -> V2AppearanceSettingsScreen(onBack = onBack)
113111
V2SettingsRoute.Health -> V2HealthLogsSettingsScreen(viewModel = viewModel, onBack = onBack)
114-
V2SettingsRoute.About -> V2AboutSettingsScreen(onBack = onBack)
112+
V2SettingsRoute.About -> V2AboutSettingsScreen(viewModel = viewModel, onBack = onBack)
115113
}
116114
}
117115

@@ -456,22 +454,72 @@ private fun V2AppearanceSettingsScreen(onBack: () -> Unit) {
456454
}
457455

458456
@Composable
459-
private fun V2AboutSettingsScreen(onBack: () -> Unit) {
457+
private fun V2AboutSettingsScreen(
458+
viewModel: MainViewModel,
459+
onBack: () -> Unit,
460+
) {
461+
val isConnected by viewModel.isConnected.collectAsState()
462+
val serverName by viewModel.serverName.collectAsState()
463+
val gatewayVersion by viewModel.gatewayVersion.collectAsState()
464+
val updateAvailable by viewModel.gatewayUpdateAvailable.collectAsState()
465+
val latestVersion = updateAvailable?.latestVersion?.takeIf { it.isNotBlank() }
466+
val currentGatewayVersion = updateAvailable?.currentVersion?.takeIf { it.isNotBlank() } ?: gatewayVersion
467+
460468
V2SettingsDetailFrame(title = "About", subtitle = "OpenClaw for Android.", icon = Icons.Default.Info, onBack = onBack) {
461469
V2SettingsMetricPanel(
462470
rows =
463471
listOf(
464-
V2SettingsMetric("Version", BuildConfig.VERSION_NAME),
472+
V2SettingsMetric("Android App", BuildConfig.VERSION_NAME),
465473
V2SettingsMetric("Build", BuildConfig.VERSION_CODE.toString()),
466474
V2SettingsMetric("Channel", "Play"),
475+
V2SettingsMetric("Gateway", currentGatewayVersion ?: "Not connected"),
467476
),
468477
)
478+
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
479+
Column {
480+
V2AboutStatusRow(title = "Gateway", value = serverName?.takeIf { it.isNotBlank() } ?: "Home Gateway", healthy = isConnected)
481+
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
482+
V2AboutStatusRow(title = "Runtime", value = currentGatewayVersion ?: "Waiting", healthy = currentGatewayVersion != null)
483+
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
484+
V2AboutStatusRow(
485+
title = "Update",
486+
value = latestVersion?.let { "v$it available" } ?: "Up to date",
487+
healthy = latestVersion == null,
488+
)
489+
}
490+
}
469491
ClawPanel {
470-
Text(text = "OpenClaw turns this phone into a clean mobile command surface for your sessions, voice, providers, and Gateway.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
492+
Text(text = aboutUpdateText(latestVersion = latestVersion), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
493+
}
494+
}
495+
}
496+
497+
@Composable
498+
private fun V2AboutStatusRow(
499+
title: String,
500+
value: String,
501+
healthy: Boolean,
502+
) {
503+
Row(
504+
modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 7.dp),
505+
verticalAlignment = Alignment.CenterVertically,
506+
horizontalArrangement = Arrangement.spacedBy(9.dp),
507+
) {
508+
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
509+
Text(text = title, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1)
510+
Text(text = value, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
471511
}
512+
ClawStatusPill(text = if (healthy) "OK" else "Check", status = if (healthy) ClawStatus.Success else ClawStatus.Warning)
472513
}
473514
}
474515

516+
private fun aboutUpdateText(latestVersion: String?): String =
517+
if (latestVersion == null) {
518+
"OpenClaw turns this phone into a clean mobile command surface for sessions, voice, providers, and Gateway."
519+
} else {
520+
"A Gateway update is available. Run the update from the Web UI or CLI when you are ready."
521+
}
522+
475523
@Composable
476524
internal fun V2SettingsDetailFrame(
477525
title: String,
@@ -810,24 +858,6 @@ internal fun V2SettingsMetricPanel(rows: List<V2SettingsMetric>) {
810858
}
811859
}
812860

813-
@Composable
814-
private fun V2HealthRow(
815-
title: String,
816-
value: String,
817-
healthy: Boolean,
818-
) {
819-
ClawPanel(contentPadding = PaddingValues(horizontal = 10.dp, vertical = 8.dp)) {
820-
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
821-
Box(modifier = Modifier.size(7.dp).clip(CircleShape).background(if (healthy) ClawTheme.colors.success else ClawTheme.colors.warning))
822-
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
823-
Text(text = title, style = ClawTheme.type.section, color = ClawTheme.colors.text)
824-
Text(text = value, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted, maxLines = 2, overflow = TextOverflow.Ellipsis)
825-
}
826-
ClawStatusPill(text = if (healthy) "OK" else "Check", status = if (healthy) ClawStatus.Success else ClawStatus.Warning)
827-
}
828-
}
829-
}
830-
831861
@Composable
832862
private fun V2SettingsBackButton(onClick: () -> Unit) {
833863
Surface(onClick = onClick, modifier = Modifier.size(30.dp), shape = CircleShape, color = Color.Transparent, contentColor = ClawTheme.colors.text) {

0 commit comments

Comments
 (0)