mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
feat(android): add v2 about update status
This commit is contained in:
@@ -5,6 +5,7 @@ import ai.openclaw.app.chat.ChatPendingToolCall
|
||||
import ai.openclaw.app.chat.ChatSessionEntry
|
||||
import ai.openclaw.app.chat.OutgoingAttachment
|
||||
import ai.openclaw.app.gateway.GatewayEndpoint
|
||||
import ai.openclaw.app.gateway.GatewayUpdateAvailableSummary
|
||||
import ai.openclaw.app.node.CameraCaptureManager
|
||||
import ai.openclaw.app.node.CanvasController
|
||||
import ai.openclaw.app.node.SmsManager
|
||||
@@ -81,6 +82,8 @@ class MainViewModel(
|
||||
val statusText: StateFlow<String> = runtimeState(initial = "Offline") { it.statusText }
|
||||
val serverName: StateFlow<String?> = runtimeState(initial = null) { it.serverName }
|
||||
val remoteAddress: StateFlow<String?> = runtimeState(initial = null) { it.remoteAddress }
|
||||
val gatewayVersion: StateFlow<String?> = runtimeState(initial = null) { it.gatewayVersion }
|
||||
val gatewayUpdateAvailable: StateFlow<GatewayUpdateAvailableSummary?> = runtimeState(initial = null) { it.gatewayUpdateAvailable }
|
||||
val modelCatalog: StateFlow<List<GatewayModelSummary>> = runtimeState(initial = emptyList()) { it.modelCatalog }
|
||||
val modelAuthProviders: StateFlow<List<GatewayModelProviderSummary>> = runtimeState(initial = emptyList()) { it.modelAuthProviders }
|
||||
val modelCatalogRefreshing: StateFlow<Boolean> = runtimeState(initial = false) { it.modelCatalogRefreshing }
|
||||
|
||||
@@ -12,6 +12,7 @@ import ai.openclaw.app.gateway.GatewayEndpoint
|
||||
import ai.openclaw.app.gateway.GatewaySession
|
||||
import ai.openclaw.app.gateway.GatewayTlsProbeFailure
|
||||
import ai.openclaw.app.gateway.GatewayTlsProbeResult
|
||||
import ai.openclaw.app.gateway.GatewayUpdateAvailableSummary
|
||||
import ai.openclaw.app.gateway.normalizeGatewayTlsFingerprint
|
||||
import ai.openclaw.app.gateway.probeGatewayTlsFingerprint
|
||||
import ai.openclaw.app.node.A2UIHandler
|
||||
@@ -295,6 +296,12 @@ class NodeRuntime(
|
||||
private val _remoteAddress = MutableStateFlow<String?>(null)
|
||||
val remoteAddress: StateFlow<String?> = _remoteAddress.asStateFlow()
|
||||
|
||||
private val _gatewayVersion = MutableStateFlow<String?>(null)
|
||||
val gatewayVersion: StateFlow<String?> = _gatewayVersion.asStateFlow()
|
||||
|
||||
private val _gatewayUpdateAvailable = MutableStateFlow<GatewayUpdateAvailableSummary?>(null)
|
||||
val gatewayUpdateAvailable: StateFlow<GatewayUpdateAvailableSummary?> = _gatewayUpdateAvailable.asStateFlow()
|
||||
|
||||
private val _seamColorArgb = MutableStateFlow(DEFAULT_SEAM_COLOR_ARGB)
|
||||
val seamColorArgb: StateFlow<Long> = _seamColorArgb.asStateFlow()
|
||||
private val _modelCatalog = MutableStateFlow<List<GatewayModelSummary>>(emptyList())
|
||||
@@ -377,13 +384,15 @@ class NodeRuntime(
|
||||
scope = scope,
|
||||
identityStore = identityStore,
|
||||
deviceAuthStore = deviceAuthStore,
|
||||
onConnected = { name, remote, mainSessionKey ->
|
||||
onConnected = { hello ->
|
||||
operatorConnected = true
|
||||
operatorStatusText = "Connected"
|
||||
_serverName.value = name
|
||||
_remoteAddress.value = remote
|
||||
_serverName.value = hello.serverName
|
||||
_remoteAddress.value = hello.remoteAddress
|
||||
_gatewayVersion.value = hello.serverVersion
|
||||
_gatewayUpdateAvailable.value = hello.updateAvailable
|
||||
_seamColorArgb.value = DEFAULT_SEAM_COLOR_ARGB
|
||||
syncMainSessionKey(resolveAgentIdFromMainSessionKey(mainSessionKey))
|
||||
syncMainSessionKey(resolveAgentIdFromMainSessionKey(hello.mainSessionKey))
|
||||
updateStatus()
|
||||
micCapture.onGatewayConnectionChanged(true)
|
||||
scope.launch {
|
||||
@@ -398,6 +407,8 @@ class NodeRuntime(
|
||||
operatorStatusText = message
|
||||
_serverName.value = null
|
||||
_remoteAddress.value = null
|
||||
_gatewayVersion.value = null
|
||||
_gatewayUpdateAvailable.value = null
|
||||
_seamColorArgb.value = DEFAULT_SEAM_COLOR_ARGB
|
||||
_modelCatalog.value = emptyList()
|
||||
_modelAuthProviders.value = emptyList()
|
||||
@@ -429,7 +440,7 @@ class NodeRuntime(
|
||||
scope = scope,
|
||||
identityStore = identityStore,
|
||||
deviceAuthStore = deviceAuthStore,
|
||||
onConnected = { _, _, _ ->
|
||||
onConnected = {
|
||||
_nodeConnected.value = true
|
||||
nodeStatusText = "Connected"
|
||||
didAutoRequestCanvasRehydrate = false
|
||||
@@ -1585,11 +1596,28 @@ class NodeRuntime(
|
||||
event: String,
|
||||
payloadJson: String?,
|
||||
) {
|
||||
if (event == "update.available") {
|
||||
_gatewayUpdateAvailable.value = parseGatewayUpdateAvailable(payloadJson)
|
||||
}
|
||||
micCapture.handleGatewayEvent(event, payloadJson)
|
||||
talkMode.handleGatewayEvent(event, payloadJson)
|
||||
chat.handleGatewayEvent(event, payloadJson)
|
||||
}
|
||||
|
||||
private fun parseGatewayUpdateAvailable(payloadJson: String?): GatewayUpdateAvailableSummary? {
|
||||
return try {
|
||||
val root = payloadJson?.let { json.parseToJsonElement(it).asObjectOrNull() }
|
||||
val update = root?.get("updateAvailable").asObjectOrNull() ?: return null
|
||||
GatewayUpdateAvailableSummary(
|
||||
currentVersion = update["currentVersion"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
|
||||
latestVersion = update["latestVersion"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
|
||||
channel = update["channel"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() },
|
||||
)
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseChatSendRunId(response: String): String? {
|
||||
return try {
|
||||
val root = json.parseToJsonElement(response).asObjectOrNull() ?: return null
|
||||
|
||||
@@ -68,6 +68,20 @@ data class GatewayConnectErrorDetails(
|
||||
val reason: String? = null,
|
||||
)
|
||||
|
||||
data class GatewayHelloSummary(
|
||||
val serverName: String?,
|
||||
val remoteAddress: String?,
|
||||
val serverVersion: String?,
|
||||
val mainSessionKey: String?,
|
||||
val updateAvailable: GatewayUpdateAvailableSummary?,
|
||||
)
|
||||
|
||||
data class GatewayUpdateAvailableSummary(
|
||||
val currentVersion: String?,
|
||||
val latestVersion: String?,
|
||||
val channel: String?,
|
||||
)
|
||||
|
||||
private data class SelectedConnectAuth(
|
||||
val authToken: String?,
|
||||
val authBootstrapToken: String?,
|
||||
@@ -86,7 +100,7 @@ class GatewaySession(
|
||||
private val scope: CoroutineScope,
|
||||
private val identityStore: DeviceIdentityStore,
|
||||
private val deviceAuthStore: DeviceAuthTokenStore,
|
||||
private val onConnected: (serverName: String?, remoteAddress: String?, mainSessionKey: String?) -> Unit,
|
||||
private val onConnected: (GatewayHelloSummary) -> Unit,
|
||||
private val onDisconnected: (message: String) -> Unit,
|
||||
private val onEvent: (event: String, payloadJson: String?) -> Unit,
|
||||
private val onInvoke: (suspend (InvokeRequest) -> InvokeResult)? = null,
|
||||
@@ -647,7 +661,9 @@ class GatewaySession(
|
||||
pendingDeviceTokenRetry = false
|
||||
deviceTokenRetryBudgetUsed = false
|
||||
reconnectPausedForAuthFailure = false
|
||||
val serverName = obj["server"].asObjectOrNull()?.get("host").asStringOrNull()
|
||||
val server = obj["server"].asObjectOrNull()
|
||||
val serverName = server?.get("host").asStringOrNull()
|
||||
val serverVersion = server?.get("version").asStringOrNull()
|
||||
val authObj = obj["auth"].asObjectOrNull()
|
||||
val deviceToken = authObj?.get("deviceToken").asStringOrNull()
|
||||
val authRole = authObj?.get("role").asStringOrNull() ?: options.role
|
||||
@@ -685,13 +701,33 @@ class GatewaySession(
|
||||
?.let { normalized -> surface to normalized }
|
||||
} ?: emptyList()
|
||||
pluginSurfaceUrls = normalizedPluginSurfaceUrls.toMap()
|
||||
val snapshot = obj["snapshot"].asObjectOrNull()
|
||||
val sessionDefaults =
|
||||
obj["snapshot"]
|
||||
.asObjectOrNull()
|
||||
snapshot
|
||||
?.get("sessionDefaults")
|
||||
.asObjectOrNull()
|
||||
mainSessionKey = sessionDefaults?.get("mainSessionKey").asStringOrNull()
|
||||
onConnected(serverName, remoteAddress, mainSessionKey)
|
||||
onConnected(
|
||||
GatewayHelloSummary(
|
||||
serverName = serverName,
|
||||
remoteAddress = remoteAddress,
|
||||
serverVersion = serverVersion,
|
||||
mainSessionKey = mainSessionKey,
|
||||
updateAvailable = parseUpdateAvailable(snapshot?.get("updateAvailable").asObjectOrNull()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseUpdateAvailable(value: JsonObject?): GatewayUpdateAvailableSummary? {
|
||||
if (value == null) return null
|
||||
val latestVersion = value["latestVersion"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
|
||||
val currentVersion = value["currentVersion"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
|
||||
val channel = value["channel"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
|
||||
return GatewayUpdateAvailableSummary(
|
||||
currentVersion = currentVersion,
|
||||
latestVersion = latestVersion,
|
||||
channel = channel,
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildConnectParams(
|
||||
|
||||
@@ -18,7 +18,6 @@ import ai.openclaw.app.ui.design.ClawStatusPill
|
||||
import ai.openclaw.app.ui.design.ClawTextField
|
||||
import ai.openclaw.app.ui.design.ClawTheme
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -60,7 +59,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -111,7 +109,7 @@ internal fun V2SettingsDetailScreen(
|
||||
V2SettingsRoute.Gateway -> V2GatewaySettingsScreen(viewModel = viewModel, onBack = onBack)
|
||||
V2SettingsRoute.Appearance -> V2AppearanceSettingsScreen(onBack = onBack)
|
||||
V2SettingsRoute.Health -> V2HealthLogsSettingsScreen(viewModel = viewModel, onBack = onBack)
|
||||
V2SettingsRoute.About -> V2AboutSettingsScreen(onBack = onBack)
|
||||
V2SettingsRoute.About -> V2AboutSettingsScreen(viewModel = viewModel, onBack = onBack)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,22 +454,72 @@ private fun V2AppearanceSettingsScreen(onBack: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun V2AboutSettingsScreen(onBack: () -> Unit) {
|
||||
private fun V2AboutSettingsScreen(
|
||||
viewModel: MainViewModel,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val isConnected by viewModel.isConnected.collectAsState()
|
||||
val serverName by viewModel.serverName.collectAsState()
|
||||
val gatewayVersion by viewModel.gatewayVersion.collectAsState()
|
||||
val updateAvailable by viewModel.gatewayUpdateAvailable.collectAsState()
|
||||
val latestVersion = updateAvailable?.latestVersion?.takeIf { it.isNotBlank() }
|
||||
val currentGatewayVersion = updateAvailable?.currentVersion?.takeIf { it.isNotBlank() } ?: gatewayVersion
|
||||
|
||||
V2SettingsDetailFrame(title = "About", subtitle = "OpenClaw for Android.", icon = Icons.Default.Info, onBack = onBack) {
|
||||
V2SettingsMetricPanel(
|
||||
rows =
|
||||
listOf(
|
||||
V2SettingsMetric("Version", BuildConfig.VERSION_NAME),
|
||||
V2SettingsMetric("Android App", BuildConfig.VERSION_NAME),
|
||||
V2SettingsMetric("Build", BuildConfig.VERSION_CODE.toString()),
|
||||
V2SettingsMetric("Channel", "Play"),
|
||||
V2SettingsMetric("Gateway", currentGatewayVersion ?: "Not connected"),
|
||||
),
|
||||
)
|
||||
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
|
||||
Column {
|
||||
V2AboutStatusRow(title = "Gateway", value = serverName?.takeIf { it.isNotBlank() } ?: "Home Gateway", healthy = isConnected)
|
||||
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
|
||||
V2AboutStatusRow(title = "Runtime", value = currentGatewayVersion ?: "Waiting", healthy = currentGatewayVersion != null)
|
||||
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
|
||||
V2AboutStatusRow(
|
||||
title = "Update",
|
||||
value = latestVersion?.let { "v$it available" } ?: "Up to date",
|
||||
healthy = latestVersion == null,
|
||||
)
|
||||
}
|
||||
}
|
||||
ClawPanel {
|
||||
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)
|
||||
Text(text = aboutUpdateText(latestVersion = latestVersion), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun V2AboutStatusRow(
|
||||
title: String,
|
||||
value: String,
|
||||
healthy: Boolean,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
|
||||
Text(text = title, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1)
|
||||
Text(text = value, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
ClawStatusPill(text = if (healthy) "OK" else "Check", status = if (healthy) ClawStatus.Success else ClawStatus.Warning)
|
||||
}
|
||||
}
|
||||
|
||||
private fun aboutUpdateText(latestVersion: String?): String =
|
||||
if (latestVersion == null) {
|
||||
"OpenClaw turns this phone into a clean mobile command surface for sessions, voice, providers, and Gateway."
|
||||
} else {
|
||||
"A Gateway update is available. Run the update from the Web UI or CLI when you are ready."
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun V2SettingsDetailFrame(
|
||||
title: String,
|
||||
@@ -810,24 +858,6 @@ internal fun V2SettingsMetricPanel(rows: List<V2SettingsMetric>) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun V2HealthRow(
|
||||
title: String,
|
||||
value: String,
|
||||
healthy: Boolean,
|
||||
) {
|
||||
ClawPanel(contentPadding = PaddingValues(horizontal = 10.dp, vertical = 8.dp)) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Box(modifier = Modifier.size(7.dp).clip(CircleShape).background(if (healthy) ClawTheme.colors.success else ClawTheme.colors.warning))
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(text = title, style = ClawTheme.type.section, color = ClawTheme.colors.text)
|
||||
Text(text = value, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
ClawStatusPill(text = if (healthy) "OK" else "Check", status = if (healthy) ClawStatus.Success else ClawStatus.Warning)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun V2SettingsBackButton(onClick: () -> Unit) {
|
||||
Surface(onClick = onClick, modifier = Modifier.size(30.dp), shape = CircleShape, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
|
||||
|
||||
Reference in New Issue
Block a user