Skip to content

Commit 5de8f8e

Browse files
committed
feat(android): polish v2 voice surfaces
1 parent 338a006 commit 5de8f8e

2 files changed

Lines changed: 188 additions & 15 deletions

File tree

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

Lines changed: 133 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import ai.openclaw.app.ui.design.ClawTextBadge
2222
import ai.openclaw.app.ui.design.ClawTextField
2323
import ai.openclaw.app.ui.design.ClawTheme
2424
import androidx.compose.foundation.BorderStroke
25+
import androidx.compose.foundation.background
2526
import androidx.compose.foundation.layout.Arrangement
2627
import androidx.compose.foundation.layout.Box
2728
import androidx.compose.foundation.layout.Column
@@ -35,20 +36,24 @@ import androidx.compose.foundation.layout.padding
3536
import androidx.compose.foundation.layout.size
3637
import androidx.compose.foundation.lazy.LazyColumn
3738
import androidx.compose.foundation.shape.CircleShape
39+
import androidx.compose.foundation.shape.RoundedCornerShape
3840
import androidx.compose.material.icons.Icons
3941
import androidx.compose.material.icons.automirrored.filled.ArrowBack
42+
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
4043
import androidx.compose.material.icons.automirrored.filled.ScreenShare
4144
import androidx.compose.material.icons.automirrored.filled.VolumeUp
4245
import androidx.compose.material.icons.filled.Bolt
4346
import androidx.compose.material.icons.filled.CameraAlt
4447
import androidx.compose.material.icons.filled.Cloud
48+
import androidx.compose.material.icons.filled.GraphicEq
4549
import androidx.compose.material.icons.filled.Info
4650
import androidx.compose.material.icons.filled.LocationOn
4751
import androidx.compose.material.icons.filled.Lock
4852
import androidx.compose.material.icons.filled.Mic
4953
import androidx.compose.material.icons.filled.Notifications
5054
import androidx.compose.material.icons.filled.Palette
5155
import androidx.compose.material.icons.filled.Person
56+
import androidx.compose.material.icons.filled.PlayArrow
5257
import androidx.compose.material.icons.filled.Storage
5358
import androidx.compose.material3.HorizontalDivider
5459
import androidx.compose.material3.Icon
@@ -389,21 +394,139 @@ private fun V2VoiceSettingsScreen(
389394
val speakerEnabled by viewModel.speakerEnabled.collectAsState()
390395
val micEnabled by viewModel.micEnabled.collectAsState()
391396
val talkModeEnabled by viewModel.talkModeEnabled.collectAsState()
392-
val micStatusText by viewModel.micStatusText.collectAsState()
393-
val talkModeStatusText by viewModel.talkModeStatusText.collectAsState()
394397

395-
V2SettingsDetailFrame(title = "Voice", subtitle = "Control talk, dictation, and playback.", icon = Icons.Default.Mic, onBack = onBack) {
396-
V2SettingsTogglePanel(
397-
rows =
398-
listOf(
399-
V2SettingsToggleRow("Speaker", if (speakerEnabled) "Assistant replies play aloud." else "Assistant speech is muted.", Icons.AutoMirrored.Filled.VolumeUp, speakerEnabled, viewModel::setSpeakerEnabled),
400-
V2SettingsToggleRow("Dictation", micStatusText, Icons.Default.Mic, micEnabled, viewModel::setMicEnabled),
401-
V2SettingsToggleRow("Realtime Talk", talkModeStatusText, Icons.Default.Bolt, talkModeEnabled, viewModel::setTalkModeEnabled),
402-
),
398+
V2SettingsDetailFrame(title = "Talk Provider Setup", subtitle = "Configure voice, transport, and playback.", icon = Icons.Default.Mic, onBack = onBack) {
399+
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
400+
V2VoiceSetupPanel(
401+
voiceActive = micEnabled || talkModeEnabled,
402+
)
403+
Text(text = "Audio Test", style = ClawTheme.type.section, color = ClawTheme.colors.text)
404+
Text(text = "Check that OpenClaw can speak clearly on this phone.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
405+
V2SettingsWaveformPanel(active = speakerEnabled)
406+
V2VoiceSetupActionRow(
407+
title = if (speakerEnabled) "Mute speaker" else "Enable speaker",
408+
subtitle = if (speakerEnabled) "Replies play aloud" else "Assistant speech muted",
409+
icon = Icons.AutoMirrored.Filled.VolumeUp,
410+
statusText = if (speakerEnabled) "On" else "Muted",
411+
ready = speakerEnabled,
412+
onClick = { viewModel.setSpeakerEnabled(!speakerEnabled) },
413+
)
414+
ClawPrimaryButton(text = "Save Voice Setup", onClick = onBack, modifier = Modifier.fillMaxWidth(), icon = Icons.Default.GraphicEq)
415+
}
416+
}
417+
}
418+
419+
@Composable
420+
private fun V2VoiceSetupPanel(
421+
voiceActive: Boolean,
422+
) {
423+
Column(verticalArrangement = Arrangement.spacedBy(9.dp)) {
424+
V2VoiceSetupActionRow(
425+
title = "Realtime Provider",
426+
subtitle = "Gateway voice relay",
427+
icon = Icons.Default.GraphicEq,
428+
statusText = if (voiceActive) "Live" else "Ready",
429+
ready = true,
430+
)
431+
V2VoiceSetupActionRow(
432+
title = "Voice",
433+
subtitle = "Voice input",
434+
icon = Icons.Default.Mic,
435+
statusText = "Configured",
436+
ready = true,
437+
)
438+
V2VoiceSetupActionRow(
439+
title = "Transport",
440+
subtitle = "Socket relay",
441+
icon = Icons.Default.Bolt,
442+
statusText = "Configured",
443+
ready = true,
403444
)
404445
}
405446
}
406447

448+
@Composable
449+
private fun V2VoiceSetupActionRow(
450+
title: String,
451+
subtitle: String,
452+
icon: ImageVector,
453+
statusText: String,
454+
ready: Boolean,
455+
onClick: (() -> Unit)? = null,
456+
) {
457+
val rowModifier = Modifier.fillMaxWidth().heightIn(min = 68.dp)
458+
Surface(
459+
onClick = onClick ?: {},
460+
enabled = onClick != null,
461+
modifier = rowModifier,
462+
shape = RoundedCornerShape(ClawTheme.radii.panel),
463+
color = ClawTheme.colors.surface,
464+
contentColor = ClawTheme.colors.text,
465+
border = BorderStroke(1.dp, ClawTheme.colors.border),
466+
) {
467+
Row(
468+
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 10.dp),
469+
verticalAlignment = Alignment.CenterVertically,
470+
horizontalArrangement = Arrangement.spacedBy(11.dp),
471+
) {
472+
Surface(
473+
modifier = Modifier.size(38.dp),
474+
shape = CircleShape,
475+
color = ClawTheme.colors.canvas,
476+
contentColor = ClawTheme.colors.text,
477+
border = BorderStroke(1.dp, ClawTheme.colors.borderStrong),
478+
) {
479+
Box(contentAlignment = Alignment.Center) {
480+
Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(19.dp))
481+
}
482+
}
483+
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
484+
Text(text = title, style = ClawTheme.type.section, color = ClawTheme.colors.text, maxLines = 1, overflow = TextOverflow.Ellipsis)
485+
Text(text = subtitle, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
486+
}
487+
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(7.dp)) {
488+
Box(
489+
modifier =
490+
Modifier
491+
.size(7.dp)
492+
.background(if (ready) ClawTheme.colors.success else ClawTheme.colors.textSubtle, CircleShape),
493+
)
494+
Text(text = statusText, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted, maxLines = 1)
495+
Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null, modifier = Modifier.size(20.dp), tint = ClawTheme.colors.textMuted)
496+
}
497+
}
498+
}
499+
}
500+
501+
@Composable
502+
private fun V2SettingsWaveformPanel(active: Boolean) {
503+
Surface(
504+
modifier = Modifier.fillMaxWidth().height(76.dp),
505+
shape = RoundedCornerShape(ClawTheme.radii.panel),
506+
color = ClawTheme.colors.surface,
507+
contentColor = ClawTheme.colors.text,
508+
border = BorderStroke(1.dp, ClawTheme.colors.border),
509+
) {
510+
Row(
511+
modifier = Modifier.fillMaxWidth().padding(horizontal = 18.dp),
512+
verticalAlignment = Alignment.CenterVertically,
513+
horizontalArrangement = Arrangement.spacedBy(5.dp),
514+
) {
515+
Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null, modifier = Modifier.size(24.dp), tint = ClawTheme.colors.text)
516+
Row(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically) {
517+
listOf(6, 12, 18, 11, 28, 34, 18, 10, 8, 24, 38, 31, 12, 8, 18, 30, 40, 22, 12, 8, 20, 29, 16, 8).forEachIndexed { index, height ->
518+
Box(
519+
modifier =
520+
Modifier
521+
.size(width = 2.dp, height = (if (active) height else 7 + index % 4 * 4).dp)
522+
.background(if (active) ClawTheme.colors.text else ClawTheme.colors.textSubtle, RoundedCornerShape(999.dp)),
523+
)
524+
}
525+
}
526+
}
527+
}
528+
}
529+
407530
@Composable
408531
private fun V2NotificationSettingsScreen(
409532
viewModel: MainViewModel,

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

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import androidx.compose.foundation.shape.CircleShape
3737
import androidx.compose.foundation.shape.RoundedCornerShape
3838
import androidx.compose.material.icons.Icons
3939
import androidx.compose.material.icons.automirrored.filled.ArrowBack
40+
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
4041
import androidx.compose.material.icons.automirrored.filled.Send
4142
import androidx.compose.material.icons.automirrored.filled.VolumeOff
4243
import androidx.compose.material.icons.automirrored.filled.VolumeUp
@@ -160,6 +161,7 @@ fun V2VoiceScreen(viewModel: MainViewModel) {
160161
)
161162

162163
V2VoiceHero(
164+
gatewayStatus = gatewayStatus,
163165
voiceCaptureMode = voiceCaptureMode,
164166
micEnabled = micEnabled,
165167
talkModeEnabled = talkModeEnabled,
@@ -563,6 +565,7 @@ private fun V2VoicePlainIconButton(
563565

564566
@Composable
565567
private fun V2VoiceHero(
568+
gatewayStatus: String,
566569
voiceCaptureMode: VoiceCaptureMode,
567570
micEnabled: Boolean,
568571
talkModeEnabled: Boolean,
@@ -572,7 +575,7 @@ private fun V2VoiceHero(
572575
onStartTalk: () -> Unit,
573576
onStartDictation: () -> Unit,
574577
) {
575-
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp)) {
578+
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp)) {
576579
V2VoiceOrb(
577580
active = micEnabled || talkModeEnabled,
578581
listening = talkModeListening || voiceCaptureMode == VoiceCaptureMode.ManualMic,
@@ -633,6 +636,8 @@ private fun V2VoiceHero(
633636
)
634637
}
635638

639+
V2VoiceProviderCard(gatewayStatus = gatewayStatus)
640+
636641
V2VoicePrimaryAction(
637642
text = if (talkModeEnabled) "End Talk" else "Start Talk",
638643
icon = if (talkModeEnabled) Icons.Default.PhoneDisabled else Icons.Default.Phone,
@@ -650,12 +655,12 @@ private fun V2VoiceModeRow(
650655
) {
651656
Surface(onClick = onClick, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
652657
Row(
653-
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp).padding(horizontal = 9.dp, vertical = 6.dp),
658+
modifier = Modifier.fillMaxWidth().heightIn(min = 60.dp).padding(horizontal = 10.dp, vertical = 6.dp),
654659
verticalAlignment = Alignment.CenterVertically,
655660
horizontalArrangement = Arrangement.spacedBy(10.dp),
656661
) {
657662
Surface(
658-
modifier = Modifier.size(30.dp),
663+
modifier = Modifier.size(34.dp),
659664
shape = CircleShape,
660665
color = ClawTheme.colors.surface,
661666
contentColor = ClawTheme.colors.text,
@@ -669,6 +674,51 @@ private fun V2VoiceModeRow(
669674
Text(text = title, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1)
670675
Text(text = subtitle, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
671676
}
677+
Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null, modifier = Modifier.size(21.dp), tint = ClawTheme.colors.textMuted)
678+
}
679+
}
680+
}
681+
682+
@Composable
683+
private fun V2VoiceProviderCard(gatewayStatus: String) {
684+
val ready = gatewayStatus.isVoiceGatewayReady()
685+
Surface(
686+
modifier = Modifier.fillMaxWidth().heightIn(min = 58.dp),
687+
shape = RoundedCornerShape(ClawTheme.radii.panel),
688+
color = ClawTheme.colors.surface,
689+
contentColor = ClawTheme.colors.text,
690+
border = BorderStroke(1.dp, ClawTheme.colors.border),
691+
) {
692+
Row(
693+
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 9.dp),
694+
verticalAlignment = Alignment.CenterVertically,
695+
horizontalArrangement = Arrangement.spacedBy(10.dp),
696+
) {
697+
Surface(
698+
modifier = Modifier.size(34.dp),
699+
shape = CircleShape,
700+
color = ClawTheme.colors.canvas,
701+
contentColor = ClawTheme.colors.text,
702+
border = BorderStroke(1.dp, ClawTheme.colors.borderStrong),
703+
) {
704+
Box(contentAlignment = Alignment.Center) {
705+
Icon(imageVector = Icons.Default.GraphicEq, contentDescription = null, modifier = Modifier.size(17.dp))
706+
}
707+
}
708+
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
709+
Text(text = "Provider", style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1)
710+
Text(text = gatewayStatus.voiceGatewayLabel(), style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
711+
}
712+
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(7.dp)) {
713+
Box(
714+
modifier =
715+
Modifier
716+
.size(7.dp)
717+
.clip(CircleShape)
718+
.background(if (ready) ClawTheme.colors.success else ClawTheme.colors.textSubtle),
719+
)
720+
Text(text = if (ready) "Ready" else "Offline", style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1)
721+
}
672722
}
673723
}
674724
}
@@ -704,7 +754,7 @@ private fun V2VoiceOrb(
704754
speaking: Boolean,
705755
) {
706756
Surface(
707-
modifier = Modifier.size(86.dp),
757+
modifier = Modifier.size(132.dp),
708758
shape = CircleShape,
709759
color = if (active) ClawTheme.colors.surfacePressed else ClawTheme.colors.surface,
710760
border = BorderStroke(1.dp, if (active) ClawTheme.colors.borderStrong else ClawTheme.colors.border),
@@ -719,7 +769,7 @@ private fun V2VoiceOrb(
719769
else -> Icons.Default.Mic
720770
},
721771
contentDescription = null,
722-
modifier = Modifier.size(26.dp),
772+
modifier = Modifier.size(38.dp),
723773
tint = ClawTheme.colors.text,
724774
)
725775
V2Waveform(active = active)

0 commit comments

Comments
 (0)