Skip to content

Commit 6db0006

Browse files
committed
feat(android): add v2 channels settings
1 parent fd05179 commit 6db0006

5 files changed

Lines changed: 362 additions & 0 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ class MainViewModel(
101101
runtimeState(initial = GatewayNodesDevicesSummary(nodes = emptyList(), pendingDevices = emptyList(), pairedDevices = emptyList())) { it.nodesDevicesSummary }
102102
val nodesDevicesRefreshing: StateFlow<Boolean> = runtimeState(initial = false) { it.nodesDevicesRefreshing }
103103
val nodesDevicesErrorText: StateFlow<String?> = runtimeState(initial = null) { it.nodesDevicesErrorText }
104+
val channelsSummary: StateFlow<GatewayChannelsSummary> =
105+
runtimeState(initial = GatewayChannelsSummary(channels = emptyList())) { it.channelsSummary }
106+
val channelsRefreshing: StateFlow<Boolean> = runtimeState(initial = false) { it.channelsRefreshing }
107+
val channelsErrorText: StateFlow<String?> = runtimeState(initial = null) { it.channelsErrorText }
104108
val pendingGatewayTrust: StateFlow<NodeRuntime.GatewayTrustPrompt?> = runtimeState(initial = null) { it.pendingGatewayTrust }
105109
val seamColorArgb: StateFlow<Long> = runtimeState(initial = 0xFF0EA5E9) { it.seamColorArgb }
106110
val mainSessionKey: StateFlow<String> = runtimeState(initial = "main") { it.mainSessionKey }
@@ -399,6 +403,10 @@ class MainViewModel(
399403
ensureRuntime().refreshNodesDevices()
400404
}
401405

406+
fun refreshChannels() {
407+
ensureRuntime().refreshChannels()
408+
}
409+
402410
fun loadChat(sessionKey: String) {
403411
ensureRuntime().loadChat(sessionKey)
404412
}

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

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,12 @@ class NodeRuntime(
342342
val nodesDevicesRefreshing: StateFlow<Boolean> = _nodesDevicesRefreshing.asStateFlow()
343343
private val _nodesDevicesErrorText = MutableStateFlow<String?>(null)
344344
val nodesDevicesErrorText: StateFlow<String?> = _nodesDevicesErrorText.asStateFlow()
345+
private val _channelsSummary = MutableStateFlow(GatewayChannelsSummary(channels = emptyList()))
346+
val channelsSummary: StateFlow<GatewayChannelsSummary> = _channelsSummary.asStateFlow()
347+
private val _channelsRefreshing = MutableStateFlow(false)
348+
val channelsRefreshing: StateFlow<Boolean> = _channelsRefreshing.asStateFlow()
349+
private val _channelsErrorText = MutableStateFlow<String?>(null)
350+
val channelsErrorText: StateFlow<String?> = _channelsErrorText.asStateFlow()
345351

346352
private val _isForeground = MutableStateFlow(true)
347353
val isForeground: StateFlow<Boolean> = _isForeground.asStateFlow()
@@ -393,6 +399,7 @@ class NodeRuntime(
393399
pendingDevices = emptyList(),
394400
pairedDevices = emptyList(),
395401
)
402+
_channelsSummary.value = GatewayChannelsSummary(channels = emptyList())
396403
chat.applyMainSessionKey(resolveMainSessionKey())
397404
chat.onDisconnected(message)
398405
updateStatus()
@@ -663,6 +670,7 @@ class NodeRuntime(
663670
refreshUsageFromGateway()
664671
refreshSkillsFromGateway()
665672
refreshNodesDevicesFromGateway()
673+
refreshChannelsFromGateway()
666674
}
667675
}
668676

@@ -702,6 +710,12 @@ class NodeRuntime(
702710
}
703711
}
704712

713+
fun refreshChannels() {
714+
scope.launch {
715+
refreshChannelsFromGateway()
716+
}
717+
}
718+
705719
fun requestCanvasRehydrate(
706720
source: String = "manual",
707721
force: Boolean = true,
@@ -1762,6 +1776,31 @@ class NodeRuntime(
17621776
}
17631777
}
17641778

1779+
private suspend fun refreshChannelsFromGateway() {
1780+
_channelsRefreshing.value = true
1781+
_channelsErrorText.value = null
1782+
if (!operatorConnected) {
1783+
_channelsSummary.value = GatewayChannelsSummary(channels = emptyList())
1784+
_channelsRefreshing.value = false
1785+
return
1786+
}
1787+
try {
1788+
val res = operatorSession.request("channels.status", """{"probe":false,"timeoutMs":8000}""")
1789+
val root = json.parseToJsonElement(res).asObjectOrNull()
1790+
_channelsSummary.value =
1791+
GatewayChannelsSummary(
1792+
updatedAtMs = root.long("ts"),
1793+
partial = root.boolean("partial"),
1794+
warnings = parseStringArray(root?.get("warnings") as? JsonArray),
1795+
channels = parseChannelSummaries(root),
1796+
)
1797+
} catch (_: Throwable) {
1798+
_channelsErrorText.value = "Could not load channels."
1799+
} finally {
1800+
_channelsRefreshing.value = false
1801+
}
1802+
}
1803+
17651804
private fun parseGatewayModels(models: JsonArray?): List<GatewayModelSummary> =
17661805
models
17671806
?.mapNotNull { item ->
@@ -1941,6 +1980,67 @@ class NodeRuntime(
19411980
)
19421981
}.orEmpty()
19431982

1983+
private fun parseChannelSummaries(root: JsonObject?): List<GatewayChannelSummary> {
1984+
val order = parseStringArray(root?.get("channelOrder") as? JsonArray)
1985+
val labels = parseStringMap(root?.get("channelLabels").asObjectOrNull())
1986+
val channels = root?.get("channels").asObjectOrNull()
1987+
val accounts = root?.get("channelAccounts").asObjectOrNull()
1988+
val ids = (order + channels.orEmpty().keys + accounts.orEmpty().keys).distinct()
1989+
return ids
1990+
.map { id ->
1991+
val summary = channels?.get(id).asObjectOrNull()
1992+
val accountRows = parseChannelAccounts(accounts?.get(id) as? JsonArray)
1993+
GatewayChannelSummary(
1994+
id = id,
1995+
label = labels[id] ?: channelDisplayLabel(id),
1996+
accountCount = accountRows.size,
1997+
enabled = summary.boolean("enabled") || accountRows.any { it.enabled },
1998+
configured = summary.boolean("configured") || accountRows.any { it.configured },
1999+
linked = summary.boolean("linked") || accountRows.any { it.linked },
2000+
running = summary.boolean("running") || accountRows.any { it.running },
2001+
connected = summary.boolean("connected") || accountRows.any { it.connected },
2002+
error =
2003+
summary
2004+
?.get("lastError")
2005+
.asStringOrNull()
2006+
?.trim()
2007+
?.takeIf { it.isNotEmpty() }
2008+
?: accountRows.firstNotNullOfOrNull { it.error },
2009+
)
2010+
}.sortedWith(compareByDescending<GatewayChannelSummary> { it.enabled || it.configured }.thenBy { it.label.lowercase() })
2011+
}
2012+
2013+
private fun parseChannelAccounts(accounts: JsonArray?): List<GatewayChannelAccountSummary> =
2014+
accounts
2015+
?.mapNotNull { item ->
2016+
val obj = item.asObjectOrNull() ?: return@mapNotNull null
2017+
val accountId = obj["accountId"].asStringOrNull()?.trim().orEmpty()
2018+
if (accountId.isEmpty()) return@mapNotNull null
2019+
GatewayChannelAccountSummary(
2020+
enabled = obj.boolean("enabled"),
2021+
configured = obj.boolean("configured"),
2022+
linked = obj.boolean("linked"),
2023+
running = obj.boolean("running"),
2024+
connected = obj.boolean("connected"),
2025+
error =
2026+
obj["lastError"]
2027+
.asStringOrNull()
2028+
?.trim()
2029+
?.takeIf { it.isNotEmpty() },
2030+
)
2031+
}.orEmpty()
2032+
2033+
private fun parseStringMap(map: JsonObject?): Map<String, String> =
2034+
map
2035+
?.mapNotNull { (key, value) ->
2036+
value
2037+
.asStringOrNull()
2038+
?.trim()
2039+
?.takeIf { it.isNotEmpty() }
2040+
?.let { key to it }
2041+
}?.toMap()
2042+
.orEmpty()
2043+
19442044
private fun parseStringArray(items: JsonArray?): List<String> =
19452045
items
19462046
?.mapNotNull { item -> item.asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() } }
@@ -2320,6 +2420,34 @@ data class GatewayDeviceTokenSummary(
23202420
val updatedAtMs: Long?,
23212421
)
23222422

2423+
data class GatewayChannelsSummary(
2424+
val updatedAtMs: Long? = null,
2425+
val partial: Boolean = false,
2426+
val warnings: List<String> = emptyList(),
2427+
val channels: List<GatewayChannelSummary>,
2428+
)
2429+
2430+
data class GatewayChannelSummary(
2431+
val id: String,
2432+
val label: String,
2433+
val accountCount: Int,
2434+
val enabled: Boolean,
2435+
val configured: Boolean,
2436+
val linked: Boolean,
2437+
val running: Boolean,
2438+
val connected: Boolean,
2439+
val error: String?,
2440+
)
2441+
2442+
private data class GatewayChannelAccountSummary(
2443+
val enabled: Boolean,
2444+
val configured: Boolean,
2445+
val linked: Boolean,
2446+
val running: Boolean,
2447+
val connected: Boolean,
2448+
val error: String?,
2449+
)
2450+
23232451
private fun JsonObject?.long(key: String): Long? = (this?.get(key) as? JsonPrimitive)?.content?.trim()?.toLongOrNull()
23242452

23252453
private fun JsonObject?.double(key: String): Double? = (this?.get(key) as? JsonPrimitive)?.content?.trim()?.toDoubleOrNull()
@@ -2343,6 +2471,21 @@ fun providerDisplayName(provider: String): String =
23432471
.ifBlank { "Provider" }
23442472
}
23452473

2474+
fun channelDisplayLabel(channel: String): String =
2475+
when (channel.trim().lowercase()) {
2476+
"imessage" -> "iMessage"
2477+
"googlechat" -> "Google Chat"
2478+
"whatsapp" -> "WhatsApp"
2479+
else ->
2480+
channel
2481+
.replace('-', ' ')
2482+
.replace('_', ' ')
2483+
.split(' ')
2484+
.filter { it.isNotBlank() }
2485+
.joinToString(" ") { token -> token.replaceFirstChar { it.uppercase() } }
2486+
.ifBlank { "Channel" }
2487+
}
2488+
23462489
@Serializable
23472490
private data class HomeCanvasPayload(
23482491
val gatewayState: String,

0 commit comments

Comments
 (0)