Skip to content

Commit da9bf5c

Browse files
committed
fix(android): group settings by intent
1 parent eb17c0d commit da9bf5c

2 files changed

Lines changed: 130 additions & 24 deletions

File tree

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

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -898,32 +898,38 @@ private fun SettingsShellScreen(
898898
ProfilePanel(displayName = displayName.ifBlank { "OpenClaw" }, onClick = { onRouteChange(SettingsRoute.Profile) })
899899
}
900900

901-
item {
902-
SettingsGroup(
903-
rows =
904-
listOf(
905-
SettingsRow("Profile", displayName.ifBlank { "Local device" }, Icons.Default.Person, route = SettingsRoute.Profile),
906-
SettingsRow("Voice", if (speakerEnabled) "Speaker on" else "Speaker muted", Icons.Default.Mic, route = SettingsRoute.Voice),
907-
SettingsRow("Agents", if (agents.isEmpty()) "Load from gateway" else "${agents.size} available", Icons.Default.Person, status = agents.isNotEmpty(), route = SettingsRoute.Agents),
908-
SettingsRow("Approvals", approvalsSummary(pendingToolCalls.size), Icons.Default.Lock, status = approvalsStatus(pendingToolCalls.size), route = SettingsRoute.Approvals),
909-
SettingsRow("Cron Jobs", cronJobsSummary(cronStatus.jobs), Icons.Outlined.AccessTime, status = if (cronStatus.jobs > 0) cronStatus.enabled else null, route = SettingsRoute.CronJobs),
910-
SettingsRow("Usage", usageSummaryText(usageSummary.providers.size), Icons.Default.Storage, status = if (usageSummary.providers.isNotEmpty()) true else null, route = SettingsRoute.Usage),
911-
SettingsRow("Skills", skillsSummaryText(skillsSummary.skills), Icons.Default.Settings, status = skillsStatus(skillsSummary.skills), route = SettingsRoute.Skills),
912-
SettingsRow("Nodes & Devices", nodesDevicesSummaryText(nodesDevicesSummary), Icons.Default.Cloud, status = nodesDevicesStatus(nodesDevicesSummary), route = SettingsRoute.NodesDevices),
913-
SettingsRow("Channels", channelsSummaryText(channelsSummary), Icons.Default.Notifications, status = channelsStatus(channelsSummary), route = SettingsRoute.Channels),
914-
SettingsRow("Dreaming", dreamingSummaryText(dreamingSummary), Icons.Default.Storage, status = dreamingStatus(dreamingSummary), route = SettingsRoute.Dreaming),
915-
SettingsRow("Canvas", "Screen surface", Icons.AutoMirrored.Filled.ScreenShare, status = isConnected, route = SettingsRoute.Canvas),
916-
SettingsRow("Notifications", if (notificationForwardingEnabled) "Smart delivery" else "Off", Icons.Default.Notifications, route = SettingsRoute.Notifications),
917-
SettingsRow("Phone Capabilities", if (cameraEnabled) "Camera enabled" else "Locked", Icons.Default.Lock, status = !cameraEnabled, route = SettingsRoute.PhoneCapabilities),
918-
SettingsRow("Gateway", gatewaySummary(statusText, isConnected), Icons.Default.Cloud, status = isConnected, route = SettingsRoute.Gateway),
919-
SettingsRow("Appearance", appearanceThemeSummary(appearanceThemeMode), Icons.Default.Palette, route = SettingsRoute.Appearance),
920-
SettingsRow("Health", "Diagnostics", Icons.Default.Settings, status = isConnected, route = SettingsRoute.Health),
921-
SettingsRow("About", "Version and update", Icons.Default.Storage, route = SettingsRoute.About),
922-
),
923-
onOpen = onRouteChange,
901+
val settingsRows =
902+
listOf(
903+
SettingsRow("Gateway", gatewaySummary(statusText, isConnected), Icons.Default.Cloud, status = isConnected, route = SettingsRoute.Gateway),
904+
SettingsRow("Nodes & Devices", nodesDevicesSummaryText(nodesDevicesSummary), Icons.Default.Cloud, status = nodesDevicesStatus(nodesDevicesSummary), route = SettingsRoute.NodesDevices),
905+
SettingsRow("Channels", channelsSummaryText(channelsSummary), Icons.Default.Notifications, status = channelsStatus(channelsSummary), route = SettingsRoute.Channels),
906+
SettingsRow("Agents", if (agents.isEmpty()) "Load from gateway" else "${agents.size} available", Icons.Default.Person, status = agents.isNotEmpty(), route = SettingsRoute.Agents),
907+
SettingsRow("Approvals", approvalsSummary(pendingToolCalls.size), Icons.Default.Lock, status = approvalsStatus(pendingToolCalls.size), route = SettingsRoute.Approvals),
908+
SettingsRow("Cron Jobs", cronJobsSummary(cronStatus.jobs), Icons.Outlined.AccessTime, status = if (cronStatus.jobs > 0) cronStatus.enabled else null, route = SettingsRoute.CronJobs),
909+
SettingsRow("Usage", usageSummaryText(usageSummary.providers.size), Icons.Default.Storage, status = if (usageSummary.providers.isNotEmpty()) true else null, route = SettingsRoute.Usage),
910+
SettingsRow("Skills", skillsSummaryText(skillsSummary.skills), Icons.Default.Settings, status = skillsStatus(skillsSummary.skills), route = SettingsRoute.Skills),
911+
SettingsRow("Dreaming", dreamingSummaryText(dreamingSummary), Icons.Default.Storage, status = dreamingStatus(dreamingSummary), route = SettingsRoute.Dreaming),
912+
SettingsRow("Voice", if (speakerEnabled) "Speaker on" else "Speaker muted", Icons.Default.Mic, route = SettingsRoute.Voice),
913+
SettingsRow("Canvas", "Screen surface", Icons.AutoMirrored.Filled.ScreenShare, status = isConnected, route = SettingsRoute.Canvas),
914+
SettingsRow("Notifications", if (notificationForwardingEnabled) "Smart delivery" else "Off", Icons.Default.Notifications, route = SettingsRoute.Notifications),
915+
SettingsRow("Phone Capabilities", if (cameraEnabled) "Camera enabled" else "Locked", Icons.Default.Lock, status = !cameraEnabled, route = SettingsRoute.PhoneCapabilities),
916+
SettingsRow("Appearance", appearanceThemeSummary(appearanceThemeMode), Icons.Default.Palette, route = SettingsRoute.Appearance),
917+
SettingsRow("About", "Version and update", Icons.Default.Storage, route = SettingsRoute.About),
918+
SettingsRow("Health", "Diagnostics", Icons.Default.Settings, status = isConnected, route = SettingsRoute.Health),
924919
)
920+
921+
settingsSections(settingsRows).forEach { section ->
922+
item {
923+
SettingsSectionTitle(section.title)
924+
}
925+
item {
926+
SettingsGroup(rows = section.rows, onOpen = onRouteChange)
927+
}
925928
}
926929

930+
item {
931+
SettingsSectionTitle("Account")
932+
}
927933
item {
928934
SettingsGroup(
929935
rows = listOf(SettingsRow("Sign Out", "Disconnect", Icons.AutoMirrored.Filled.ExitToApp)),
@@ -1057,14 +1063,73 @@ private fun dreamingStatus(summary: GatewayDreamingSummary): Boolean? =
10571063
else -> null
10581064
}
10591065

1060-
private data class SettingsRow(
1066+
internal data class SettingsRow(
10611067
val title: String,
10621068
val value: String,
10631069
val icon: ImageVector,
10641070
val status: Boolean? = null,
10651071
val route: SettingsRoute? = null,
10661072
)
10671073

1074+
internal data class SettingsSection(
1075+
val title: String,
1076+
val rows: List<SettingsRow>,
1077+
)
1078+
1079+
internal fun settingsSections(rows: List<SettingsRow>): List<SettingsSection> =
1080+
settingsSectionOrder.mapNotNull { title ->
1081+
val sectionRows = rows.filter { row -> row.route?.let(::settingsSectionTitleForRoute) == title }
1082+
if (sectionRows.isEmpty()) null else SettingsSection(title = title, rows = sectionRows)
1083+
}
1084+
1085+
private val settingsSectionOrder =
1086+
listOf(
1087+
"Connection",
1088+
"Agents & automation",
1089+
"Phone context & privacy",
1090+
"Profile & device",
1091+
"Diagnostics",
1092+
)
1093+
1094+
internal fun settingsSectionTitleForRoute(route: SettingsRoute): String =
1095+
when (route) {
1096+
SettingsRoute.Gateway,
1097+
SettingsRoute.NodesDevices,
1098+
SettingsRoute.Channels,
1099+
-> "Connection"
1100+
1101+
SettingsRoute.Agents,
1102+
SettingsRoute.Approvals,
1103+
SettingsRoute.CronJobs,
1104+
SettingsRoute.Usage,
1105+
SettingsRoute.Skills,
1106+
SettingsRoute.Dreaming,
1107+
-> "Agents & automation"
1108+
1109+
SettingsRoute.Voice,
1110+
SettingsRoute.Canvas,
1111+
SettingsRoute.Notifications,
1112+
SettingsRoute.PhoneCapabilities,
1113+
-> "Phone context & privacy"
1114+
1115+
SettingsRoute.Profile,
1116+
SettingsRoute.Appearance,
1117+
SettingsRoute.About,
1118+
-> "Profile & device"
1119+
1120+
SettingsRoute.Health -> "Diagnostics"
1121+
SettingsRoute.Home -> "Diagnostics"
1122+
}
1123+
1124+
@Composable
1125+
private fun SettingsSectionTitle(title: String) {
1126+
Text(
1127+
text = title.uppercase(),
1128+
style = ClawTheme.type.caption.copy(fontSize = 12.sp, lineHeight = 16.sp),
1129+
color = ClawTheme.colors.textMuted,
1130+
)
1131+
}
1132+
10681133
@Composable
10691134
private fun ProfilePanel(
10701135
displayName: String,

apps/android/app/src/test/java/ai/openclaw/app/ui/ShellScreenLogicTest.kt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import ai.openclaw.app.GatewayNodeApprovalState
77
import ai.openclaw.app.GatewayNodeSummary
88
import ai.openclaw.app.GatewayNodesDevicesSummary
99
import ai.openclaw.app.GatewayPendingDeviceSummary
10+
import androidx.compose.material.icons.Icons
11+
import androidx.compose.material.icons.filled.Settings
1012
import org.junit.Assert.assertEquals
1113
import org.junit.Assert.assertFalse
1214
import org.junit.Assert.assertTrue
@@ -155,7 +157,46 @@ class ShellScreenLogicTest {
155157
assertEquals("Node approval pending", rows.single().subtitle)
156158
}
157159

160+
@Test
161+
fun settingsSectionTitlesGroupPowerSettingsByMeaning() {
162+
assertEquals("Connection", settingsSectionTitleForRoute(SettingsRoute.Gateway))
163+
assertEquals("Connection", settingsSectionTitleForRoute(SettingsRoute.NodesDevices))
164+
assertEquals("Agents & automation", settingsSectionTitleForRoute(SettingsRoute.Approvals))
165+
assertEquals("Agents & automation", settingsSectionTitleForRoute(SettingsRoute.CronJobs))
166+
assertEquals("Phone context & privacy", settingsSectionTitleForRoute(SettingsRoute.PhoneCapabilities))
167+
assertEquals("Phone context & privacy", settingsSectionTitleForRoute(SettingsRoute.Notifications))
168+
assertEquals("Profile & device", settingsSectionTitleForRoute(SettingsRoute.Appearance))
169+
assertEquals("Diagnostics", settingsSectionTitleForRoute(SettingsRoute.Health))
170+
}
171+
172+
@Test
173+
fun settingsSectionsPreserveMeaningfulOrder() {
174+
val sections =
175+
settingsSections(
176+
listOf(
177+
settingsRow(SettingsRoute.Voice),
178+
settingsRow(SettingsRoute.Agents),
179+
settingsRow(SettingsRoute.Gateway),
180+
settingsRow(SettingsRoute.Appearance),
181+
settingsRow(SettingsRoute.Health),
182+
),
183+
)
184+
185+
assertEquals(
186+
listOf(
187+
"Connection",
188+
"Agents & automation",
189+
"Phone context & privacy",
190+
"Profile & device",
191+
"Diagnostics",
192+
),
193+
sections.map { it.title },
194+
)
195+
}
196+
158197
private fun emptyChannels(): GatewayChannelsSummary = GatewayChannelsSummary(channels = emptyList())
159198

160199
private fun emptyNodesDevices(): GatewayNodesDevicesSummary = GatewayNodesDevicesSummary(nodes = emptyList(), pendingDevices = emptyList(), pairedDevices = emptyList())
200+
201+
private fun settingsRow(route: SettingsRoute): SettingsRow = SettingsRow(route.name, "Value", Icons.Default.Settings, route = route)
161202
}

0 commit comments

Comments
 (0)